mirror of
https://github.com/laurent22/joplin.git
synced 2025-03-20 20:55:18 +02:00
Server: Paginate users
This commit is contained in:
parent
8ea6d89d49
commit
8ac8d537c8
@ -10,6 +10,7 @@ import personalizedUserContentBaseUrl from '@joplin/lib/services/joplinServer/pe
|
|||||||
import Logger from '@joplin/lib/Logger';
|
import Logger from '@joplin/lib/Logger';
|
||||||
import dbuuid from '../utils/dbuuid';
|
import dbuuid from '../utils/dbuuid';
|
||||||
import { defaultPagination, PaginatedResults, Pagination } from './utils/pagination';
|
import { defaultPagination, PaginatedResults, Pagination } from './utils/pagination';
|
||||||
|
import { Knex } from 'knex';
|
||||||
import { unique } from '../utils/array';
|
import { unique } from '../utils/array';
|
||||||
|
|
||||||
const logger = Logger.create('BaseModel');
|
const logger = Logger.create('BaseModel');
|
||||||
@ -33,6 +34,10 @@ export interface LoadOptions {
|
|||||||
fields?: string[];
|
fields?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AllPaginatedOptions extends LoadOptions {
|
||||||
|
queryCallback?: (query: Knex.QueryBuilder)=> Knex.QueryBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeleteOptions {
|
export interface DeleteOptions {
|
||||||
validationRules?: any;
|
validationRules?: any;
|
||||||
allowNoOp?: boolean;
|
allowNoOp?: boolean;
|
||||||
@ -242,7 +247,7 @@ export default abstract class BaseModel<T> {
|
|||||||
return rows as T[];
|
return rows as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async allPaginated(pagination: Pagination, options: LoadOptions = {}): Promise<PaginatedResults<T>> {
|
public async allPaginated(pagination: Pagination, options: AllPaginatedOptions = {}): Promise<PaginatedResults<T>> {
|
||||||
pagination = {
|
pagination = {
|
||||||
...defaultPagination(),
|
...defaultPagination(),
|
||||||
...pagination,
|
...pagination,
|
||||||
@ -250,12 +255,18 @@ export default abstract class BaseModel<T> {
|
|||||||
|
|
||||||
const itemCount = await this.count();
|
const itemCount = await this.count();
|
||||||
|
|
||||||
const items = await this
|
let query = this
|
||||||
.db(this.tableName)
|
.db(this.tableName)
|
||||||
.select(this.selectFields(options))
|
.select(this.selectFields(options));
|
||||||
|
|
||||||
|
if (options.queryCallback) query = options.queryCallback(query);
|
||||||
|
|
||||||
|
void query
|
||||||
.orderBy(pagination.order[0].by, pagination.order[0].dir)
|
.orderBy(pagination.order[0].by, pagination.order[0].dir)
|
||||||
.offset((pagination.page - 1) * pagination.limit)
|
.offset((pagination.page - 1) * pagination.limit)
|
||||||
.limit(pagination.limit) as T[];
|
.limit(pagination.limit);
|
||||||
|
|
||||||
|
const items = (await query) as T[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
|
@ -168,7 +168,7 @@ export function createPaginationLinks(page: number, pageCount: number, urlTempla
|
|||||||
firstPages.push({ page: p });
|
firstPages.push({ page: p });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstPages.length && (output[0].page - firstPages[firstPages.length - 1].page) > 1) {
|
if (firstPages.length && output.length && (output[0].page - firstPages[firstPages.length - 1].page) > 1) {
|
||||||
firstPages.push({ isEllipsis: true });
|
firstPages.push({ isEllipsis: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { SubPath, redirect } from '../../utils/routeUtils';
|
import { SubPath, redirect } from '../../utils/routeUtils';
|
||||||
import Router from '../../utils/Router';
|
import Router from '../../utils/Router';
|
||||||
import { RouteType } from '../../utils/types';
|
import { RouteType } from '../../utils/types';
|
||||||
|
import { Knex } from 'knex';
|
||||||
import { AppContext, HttpMethod } from '../../utils/types';
|
import { AppContext, HttpMethod } from '../../utils/types';
|
||||||
import { contextSessionId, formParse } from '../../utils/requestUtils';
|
import { contextSessionId, formParse } from '../../utils/requestUtils';
|
||||||
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
|
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
|
||||||
@ -14,13 +15,15 @@ import uuidgen from '../../utils/uuidgen';
|
|||||||
import { formatMaxItemSize, formatMaxTotalSize, formatTotalSize, formatTotalSizePercent, yesOrNo } from '../../utils/strings';
|
import { formatMaxItemSize, formatMaxTotalSize, formatTotalSize, formatTotalSizePercent, yesOrNo } from '../../utils/strings';
|
||||||
import { getCanShareFolder, totalSizeClass } from '../../models/utils/user';
|
import { getCanShareFolder, totalSizeClass } from '../../models/utils/user';
|
||||||
import { yesNoDefaultOptions, yesNoOptions } from '../../utils/views/select';
|
import { yesNoDefaultOptions, yesNoOptions } from '../../utils/views/select';
|
||||||
import { stripePortalUrl, adminUserDeletionsUrl, adminUserUrl } from '../../utils/urlUtils';
|
import { stripePortalUrl, adminUserDeletionsUrl, adminUserUrl, adminUsersUrl, setQueryParameters } from '../../utils/urlUtils';
|
||||||
import { cancelSubscriptionByUserId, updateSubscriptionType } from '../../utils/stripe';
|
import { cancelSubscriptionByUserId, updateSubscriptionType } from '../../utils/stripe';
|
||||||
import { createCsrfTag } from '../../utils/csrf';
|
import { createCsrfTag } from '../../utils/csrf';
|
||||||
import { formatDateTime, Hour } from '../../utils/time';
|
import { formatDateTime, Hour } from '../../utils/time';
|
||||||
import { startImpersonating, stopImpersonating } from './utils/users/impersonate';
|
import { startImpersonating, stopImpersonating } from './utils/users/impersonate';
|
||||||
import { userFlagToString } from '../../models/UserFlagModel';
|
import { userFlagToString } from '../../models/UserFlagModel';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { makeTablePagination, makeTableView, Row, Table } from '../../utils/views/table';
|
||||||
|
import { PaginationOrderDir } from '../../models/utils/pagination';
|
||||||
|
|
||||||
export interface CheckRepeatPasswordInput {
|
export interface CheckRepeatPasswordInput {
|
||||||
password: string;
|
password: string;
|
||||||
@ -95,33 +98,96 @@ router.get('admin/users', async (_path: SubPath, ctx: AppContext) => {
|
|||||||
const userModel = ctx.joplin.models.user();
|
const userModel = ctx.joplin.models.user();
|
||||||
await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.List);
|
await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.List);
|
||||||
|
|
||||||
const users = await userModel.all();
|
const showDisabled = ctx.query.show_disabled === '1';
|
||||||
|
|
||||||
users.sort((u1: User, u2: User) => {
|
const pagination = makeTablePagination(ctx.query, 'full_name', PaginationOrderDir.ASC);
|
||||||
if (u1.full_name && u2.full_name) return u1.full_name.toLowerCase() < u2.full_name.toLowerCase() ? -1 : +1;
|
pagination.limit = 1000;
|
||||||
if (u1.full_name && !u2.full_name) return +1;
|
const page = await ctx.joplin.models.user().allPaginated(pagination, {
|
||||||
if (!u1.full_name && u2.full_name) return -1;
|
queryCallback: (query: Knex.QueryBuilder) => {
|
||||||
return u1.email.toLowerCase() < u2.email.toLowerCase() ? -1 : +1;
|
if (!showDisabled) {
|
||||||
|
void query.where('enabled', '=', 1);
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const view: View = defaultView('admin/users', _('Users'));
|
const table: Table = {
|
||||||
view.content = {
|
baseUrl: adminUsersUrl(),
|
||||||
users: users.map(user => {
|
requestQuery: ctx.query,
|
||||||
return {
|
pageCount: page.page_count,
|
||||||
...user,
|
pagination,
|
||||||
url: adminUserUrl(user.id),
|
headers: [
|
||||||
displayName: user.full_name ? user.full_name : '(not set)',
|
{
|
||||||
formattedItemMaxSize: formatMaxItemSize(user),
|
name: 'full_name',
|
||||||
formattedTotalSize: formatTotalSize(user),
|
label: _('Full name'),
|
||||||
formattedMaxTotalSize: formatMaxTotalSize(user),
|
},
|
||||||
formattedTotalSizePercent: formatTotalSizePercent(user),
|
{
|
||||||
totalSizeClass: totalSizeClass(user),
|
name: 'email',
|
||||||
formattedAccountType: accountTypeToString(user.account_type),
|
label: _('Email'),
|
||||||
formattedCanShareFolder: yesOrNo(getCanShareFolder(user)),
|
},
|
||||||
rowClassName: user.enabled ? '' : 'is-disabled',
|
{
|
||||||
|
name: 'account',
|
||||||
|
label: _('Account'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'max_item_size',
|
||||||
|
label: _('Max Item Size'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total_size',
|
||||||
|
label: _('Total Size'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'max_total_size',
|
||||||
|
label: _('Max Total Size'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'can_share',
|
||||||
|
label: _('Can Share'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rows: page.items.map(user => {
|
||||||
|
const row: Row = {
|
||||||
|
classNames: [user.enabled ? '' : 'is-disabled'],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
value: user.full_name ? user.full_name : '(not set)',
|
||||||
|
url: adminUserUrl(user.id),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: user.email,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: accountTypeToString(user.account_type),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: formatMaxItemSize(user),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `${formatTotalSize(user)} (${formatTotalSizePercent(user)})`,
|
||||||
|
classNames: [totalSizeClass(user)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: formatMaxTotalSize(user),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: yesOrNo(getCanShareFolder(user)),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return row;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const view = defaultView('admin/users', _('Users'));
|
||||||
|
view.content = {
|
||||||
|
userTable: makeTableView(table),
|
||||||
|
csrfTag: await createCsrfTag(ctx),
|
||||||
|
disabledToggleButtonLabel: showDisabled ? _('Hide disabled') : _('Show disabled'),
|
||||||
|
disabledToggleButtonUrl: setQueryParameters(adminUsersUrl(), { ...ctx.query, show_disabled: showDisabled ? '0' : '1' }),
|
||||||
|
};
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createPaginationLinks, filterPaginationQueryParams, PageLink, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, PaginationQueryParams, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
|
import { createPaginationLinks, PageLink, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, PaginationQueryParams, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
|
||||||
import { setQueryParameters } from '../urlUtils';
|
import { setQueryParameters } from '../urlUtils';
|
||||||
|
|
||||||
const defaultSortOrder = PaginationOrderDir.ASC;
|
const defaultSortOrder = PaginationOrderDir.ASC;
|
||||||
@ -45,6 +45,7 @@ interface RowItem {
|
|||||||
stretch?: boolean;
|
stretch?: boolean;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
render?: RowItemRenderCallback;
|
render?: RowItemRenderCallback;
|
||||||
|
classNames?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Row {
|
export interface Row {
|
||||||
@ -106,10 +107,13 @@ function makeRowView(row: Row): RowView {
|
|||||||
return {
|
return {
|
||||||
classNames: row.classNames,
|
classNames: row.classNames,
|
||||||
items: row.items.map(rowItem => {
|
items: row.items.map(rowItem => {
|
||||||
|
let classNames = [rowItem.stretch ? 'stretch' : 'nowrap'];
|
||||||
|
if (rowItem.classNames) classNames = classNames.concat(rowItem.classNames);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: rowItem.value,
|
value: rowItem.value,
|
||||||
valueHtml: rowItem.render ? rowItem.render() : '',
|
valueHtml: rowItem.render ? rowItem.render() : '',
|
||||||
classNames: [rowItem.stretch ? 'stretch' : 'nowrap'],
|
classNames,
|
||||||
url: rowItem.url,
|
url: rowItem.url,
|
||||||
checkbox: rowItem.checkbox,
|
checkbox: rowItem.checkbox,
|
||||||
hint: rowItem.hint,
|
hint: rowItem.hint,
|
||||||
@ -126,7 +130,7 @@ export function makeTableView(table: Table): TableView {
|
|||||||
if (table.pageCount) {
|
if (table.pageCount) {
|
||||||
if (!table.baseUrl || !table.requestQuery) throw new Error('Table.baseUrl and Table.requestQuery are required for pagination when there is more than one page');
|
if (!table.baseUrl || !table.requestQuery) throw new Error('Table.baseUrl and Table.requestQuery are required for pagination when there is more than one page');
|
||||||
|
|
||||||
baseUrlQuery = filterPaginationQueryParams(table.requestQuery);
|
baseUrlQuery = table.requestQuery; // filterPaginationQueryParams(table.requestQuery);
|
||||||
pagination = table.pagination;
|
pagination = table.pagination;
|
||||||
paginationLinks = createPaginationLinks(pagination.page, table.pageCount, setQueryParameters(table.baseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
|
paginationLinks = createPaginationLinks(pagination.page, table.pageCount, setQueryParameters(table.baseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,13 @@
|
|||||||
<div class="block">
|
<div class="block">
|
||||||
<a class="button is-primary" href="{{{global.baseUrl}}}/admin/users/new">Add user</a>
|
<a class="button is-primary" href="{{{global.baseUrl}}}/admin/users/new">Add user</a>
|
||||||
<a class="button is-link toggle-disabled-button hide-disabled" href="#">Hide disabled</a>
|
<a class="button is-link toggle-disabled-button hide-disabled" href="{{disabledToggleButtonUrl}}">{{disabledToggleButtonLabel}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table">
|
{{#userTable}}
|
||||||
<thead>
|
{{>table}}
|
||||||
<tr>
|
{{/userTable}}
|
||||||
<th>Full name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Account</th>
|
|
||||||
<th>Max Item Size</th>
|
|
||||||
<th>Total Size</th>
|
|
||||||
<th>Max Total Size</th>
|
|
||||||
<th>Can share</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#users}}
|
|
||||||
<tr class="{{rowClassName}}">
|
|
||||||
<td><a href="{{{url}}}">{{displayName}}</a></td>
|
|
||||||
<td>{{email}}</td>
|
|
||||||
<td>{{formattedAccountType}}</td>
|
|
||||||
<td>{{formattedItemMaxSize}}</td>
|
|
||||||
<td class="{{totalSizeClass}}">{{formattedTotalSize}} ({{formattedTotalSizePercent}})</td>
|
|
||||||
<td>{{formattedMaxTotalSize}}</td>
|
|
||||||
<td>{{formattedCanShareFolder}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/users}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<a class="button is-primary" href="{{{global.baseUrl}}}/admin/users/new">Add user</a>
|
<a class="button is-primary" href="{{{global.baseUrl}}}/admin/users/new">Add user</a>
|
||||||
<a class="button is-link toggle-disabled-button hide-disabled" href="#">Hide disabled</a>
|
<a class="button is-link toggle-disabled-button hide-disabled" href="{{disabledToggleButtonUrl}}">{{disabledToggleButtonLabel}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
$(() => {
|
|
||||||
function toggleDisabled() {
|
|
||||||
if ($('.hide-disabled').length) {
|
|
||||||
$('.hide-disabled').addClass('show-disabled');
|
|
||||||
$('.hide-disabled').removeClass('hide-disabled');
|
|
||||||
$('.show-disabled').text('Show disabled');
|
|
||||||
$('table tr.is-disabled').hide();
|
|
||||||
} else {
|
|
||||||
$('.show-disabled').addClass('hide-disabled');
|
|
||||||
$('.show-disabled').removeClass('show-disabled');
|
|
||||||
$('.hide-disabled').text('Hide disabled');
|
|
||||||
$('table tr.is-disabled').show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDisabled();
|
|
||||||
|
|
||||||
$('.toggle-disabled-button').click(() => {
|
|
||||||
toggleDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
Loading…
x
Reference in New Issue
Block a user