From 3cdcb07646f3fbd4537cc81f4bc1996618d19d72 Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 27 Jul 2023 15:27:14 +0300 Subject: [PATCH] CB-3268 feat: admin users pagination (#1821) * CB-3268 feat: admin users pagination * CB-3268 fix: code style * CB-3268 feat: filter users by enabled state * CB-3268 feat: limit users loading * CB-3268 fix: use limit/offset pagination * CB-3268 fix pagination api * CB-3268 fix users query * CB-3268 fix: users refresh --------- Co-authored-by: Aleksandr Skoblikov Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> --- .../src/io/cloudbeaver/WebPage.java | 28 +++++ .../schema/schema.graphqls | 5 + .../model/user/WebAuthProviderInfo.java | 1 - .../schema/service.admin.graphqls | 9 +- .../service/admin/AdminUserInfoFilter.java | 47 ++++++++ .../service/admin/DBWServiceAdmin.java | 8 +- .../service/admin/WebServiceBindingAdmin.java | 16 ++- .../service/admin/impl/WebServiceAdmin.java | 21 +++- .../CBEmbeddedSecurityController.java | 98 ++++++++++++--- .../core-authentication/src/UsersResource.ts | 109 +++++++++++++---- .../src/ResourcesHooks/usePagination.ts | 112 ++++++++++++++++++ .../src/ResourcesHooks/useResource.ts | 12 +- webapp/packages/core-blocks/src/index.ts | 1 + .../core-localization/src/locales/en.ts | 1 + .../core-localization/src/locales/it.ts | 1 + .../core-localization/src/locales/ru.ts | 1 + .../core-localization/src/locales/zh.ts | 1 + .../src/Resource/CachedMapResource.ts | 44 ++++++- .../core-sdk/src/Resource/CachedResource.ts | 19 ++- .../core-sdk/src/Resource/ResourceAlias.ts | 3 +- .../authentication/users/getUserById.gql | 5 + .../authentication/users/getUsersList.gql | 8 +- webapp/packages/core-sdk/src/sdk.ts | 76 +++++++++++- .../MetaParametersController.ts | 12 +- .../Users/Teams/GrantedUsers/GrantedUsers.tsx | 5 +- .../Users/UserForm/UserFormController.ts | 4 +- .../Users/UsersAdministrationService.ts | 11 +- .../Filters/useUsersTableFilters.tsx | 43 ++++--- .../Users/UsersTable/UserEdit.tsx | 14 +-- .../Users/UsersTable/UserEditController.ts | 65 ---------- .../Users/UsersTable/UsersPage.tsx | 35 +++--- .../Users/UsersTable/UsersTable.tsx | 48 +++++++- .../Users/UsersTable/useUsersTable.tsx | 50 +++++--- .../src/locales/en.ts | 1 + .../src/locales/it.ts | 1 + .../src/locales/ru.ts | 1 + .../src/locales/zh.ts | 1 + .../ConnectionAccess/ConnectionAccess.tsx | 4 +- webapp/packages/prettier-config/index.json | 4 +- 39 files changed, 693 insertions(+), 232 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebPage.java create mode 100644 server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfoFilter.java create mode 100644 webapp/packages/core-blocks/src/ResourcesHooks/usePagination.ts create mode 100644 webapp/packages/core-sdk/src/queries/authentication/users/getUserById.gql delete mode 100644 webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEditController.ts diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebPage.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebPage.java new file mode 100644 index 0000000000..1c21e6f1a4 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebPage.java @@ -0,0 +1,28 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver; + +import org.jkiss.dbeaver.model.DBPPage; +import org.jkiss.dbeaver.model.data.json.JSONUtils; + +import java.util.Map; + +public class WebPage extends DBPPage { + public WebPage(Map params) { + super(JSONUtils.getInteger(params, "offset"), JSONUtils.getInteger(params, "limit")); + } +} diff --git a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls index a3dee4ffa5..873430e94e 100644 --- a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls @@ -3,6 +3,11 @@ scalar Object # Date/Time scalar DateTime +input PageInput { + limit: Int + offset: Int +} + type Query type Mutation diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java index 4f3a4d5393..ec5a4591df 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java @@ -18,7 +18,6 @@ package io.cloudbeaver.model.user; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.provisioning.SMProvisioner; -import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.registry.WebAuthProviderConfiguration; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.server.CBApplication; diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index 5374f55858..39e33f6353 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -114,11 +114,16 @@ input ServerConfigInput { sessionExpireTime: Int } -extend type Query { +input AdminUserFilterInput { + userIdMask: String + enabledState: Boolean +} +extend type Query { #### Users and teams - listUsers(userId: ID): [AdminUserInfo!]! + adminUserInfo(userId: ID!): AdminUserInfo! + listUsers(page: PageInput!, filter: AdminUserFilterInput!): [AdminUserInfo!]! listTeams(teamId: ID): [AdminTeamInfo!]! listPermissions: [AdminPermissionInfo!]! listAuthRoles: [String!]! diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfoFilter.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfoFilter.java new file mode 100644 index 0000000000..52e1d90769 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfoFilter.java @@ -0,0 +1,47 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.admin; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBPPage; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.security.user.SMUserFilter; + +import java.util.Map; + +public class AdminUserInfoFilter { + @NotNull + private final SMUserFilter filter; + + public AdminUserInfoFilter(@NotNull Map params, @NotNull Map pageParams) { + DBPPage page = new DBPPage( + JSONUtils.getInteger(pageParams, "offset", 0), + JSONUtils.getInteger(pageParams, "limit", Integer.MAX_VALUE) + ); + + Boolean enabledState = params.containsKey("enabledState") ? JSONUtils.getBoolean(params, "enabledState") : null; + this.filter = new SMUserFilter( + JSONUtils.getString(params, "userIdMask"), + enabledState, + page + ); + } + + public SMUserFilter getFilter() { + return filter; + } +} diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java index aa9245fd5a..b827aeacf1 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java @@ -31,7 +31,6 @@ import org.jkiss.dbeaver.model.security.SMDataSourceGrant; import java.util.List; import java.util.Map; -import java.util.Set; /** * Web service API @@ -40,7 +39,12 @@ public interface DBWServiceAdmin extends DBWService { @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) @NotNull - List listUsers(@NotNull WebSession webSession, @Nullable String userName) throws DBWebException; + AdminUserInfo getUserById(@NotNull WebSession webSession, @NotNull String userId) throws DBWebException; + + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + @NotNull + List listUsers(@NotNull WebSession webSession, AdminUserInfoFilter filter) + throws DBWebException; @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) @NotNull diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java index d4f2a6633d..8d9a6ce927 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java @@ -31,7 +31,8 @@ import org.jkiss.utils.CommonUtils; /** * Web service implementation */ -public class WebServiceBindingAdmin extends WebServiceBindingBase implements DBWServiceBindingServlet { +public class WebServiceBindingAdmin extends WebServiceBindingBase + implements DBWServiceBindingServlet { private static final String SCHEMA_FILE_NAME = "schema/service.admin.graphqls"; @@ -42,8 +43,17 @@ public class WebServiceBindingAdmin extends WebServiceBindingBase getService(env).listUsers(getWebSession(env), env.getArgument("userId"))) + .dataFetcher( + "adminUserInfo", + env -> getService(env).getUserById(getWebSession(env), env.getArgument("userId")) + ) + .dataFetcher( + "listUsers", + env -> getService(env).listUsers( + getWebSession(env), + new AdminUserInfoFilter(env.getArgument("filter"), env.getArgument("page")) + ) + ) .dataFetcher("listTeams", env -> getService(env).listTeams(getWebSession(env), env.getArgument("teamId"))) .dataFetcher("listPermissions", diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index cdfea19417..8d712d5d01 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -22,7 +22,6 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.model.WebPropertyInfo; -import io.cloudbeaver.model.app.BaseWebApplication; import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebUser; @@ -66,16 +65,26 @@ public class WebServiceAdmin implements DBWServiceAdmin { .flatMap(service -> service.getPermissions().stream()) .collect(Collectors.toMap(WebPermissionDescriptor::getId, Function.identity())); + @NotNull + @Override + public AdminUserInfo getUserById(@NotNull WebSession webSession, @NotNull String userId) throws DBWebException { + try { + SMUser smUser = webSession.getAdminSecurityController().getUserById(userId); + return new AdminUserInfo(webSession, new WebUser(smUser)); + } catch (Exception e) { + throw new DBWebException("Error getting user - " + userId, e); + } + } @NotNull @Override - public List listUsers(@NotNull WebSession webSession, String userName) throws DBWebException { + public List listUsers(@NotNull WebSession webSession, @NotNull AdminUserInfoFilter adminFilter) throws DBWebException { try { - List webUsers = new ArrayList<>(); - for (SMUser smUser : webSession.getAdminSecurityController().findUsers(userName)) { - webUsers.add(new AdminUserInfo(webSession, new WebUser(smUser))); + List users = new ArrayList<>(); + for (SMUser smUser : webSession.getAdminSecurityController().findUsers(adminFilter.getFilter())) { + users.add(new AdminUserInfo(webSession, new WebUser(smUser))); } - return webUsers; + return users; } catch (Exception e) { throw new DBWebException("Error reading users", e); } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 44cca59a75..75b3505e2f 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -37,6 +37,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPPage; import org.jkiss.dbeaver.model.auth.*; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; @@ -49,6 +50,7 @@ import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; import org.jkiss.dbeaver.model.security.exception.SMException; import org.jkiss.dbeaver.model.security.exception.SMRefreshTokenExpiredException; import org.jkiss.dbeaver.model.security.user.*; +import org.jkiss.dbeaver.model.sql.SQLUtils; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.SecurityUtils; @@ -303,19 +305,45 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen return getUserById(getUserIdOrThrow()); } + @Override + public int countUsers(@NotNull SMUserFilter filter) throws DBCException { + try (Connection dbCon = database.openConnection()) { + try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( + "SELECT COUNT(*) FROM {table_prefix}CB_USER" + buildUsersFilter(filter)))) { + setUsersFilterValues(dbStat, filter, 1); + try (ResultSet dbResult = dbStat.executeQuery()) { + if (dbResult.next()) { + return dbResult.getInt(1); + } else { + return 0; + } + } + } + } catch (SQLException e) { + throw new DBCException("Error while counting users", e); + } + } + @NotNull @Override public SMUser[] findUsers(String userNameMask) throws DBCException { + return findUsers(new SMUserFilter(userNameMask, null, new DBPPage(0, Integer.MAX_VALUE))); + } + + @NotNull + @Override + public SMUser[] findUsers(@NotNull SMUserFilter filter) + throws DBCException { try (Connection dbCon = database.openConnection()) { Map result = new LinkedHashMap<>(); // Read users try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID,IS_ACTIVE,DEFAULT_AUTH_ROLE FROM {table_prefix}CB_USER" + - (CommonUtils.isEmpty(userNameMask) ? "\nORDER BY USER_ID" : " WHERE USER_ID=?"))) - ) { - if (!CommonUtils.isEmpty(userNameMask)) { - dbStat.setString(1, userNameMask); - } + database.normalizeTableNames("SELECT USER_ID,IS_ACTIVE,DEFAULT_AUTH_ROLE FROM {table_prefix}CB_USER" + + buildUsersFilter(filter) + "\nORDER BY USER_ID LIMIT ? OFFSET ?"))) { + int parameterIndex = setUsersFilterValues(dbStat, filter, 1); + dbStat.setInt(parameterIndex++, filter.getPage().getLimit()); + dbStat.setInt(parameterIndex++, filter.getPage().getOffset()); + try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { String userId = dbResult.getString(1); @@ -325,14 +353,22 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen } } } - readSubjectsMetas(dbCon, SMSubjectType.user, userNameMask, result); + if (result.isEmpty()) { + return new SMUser[0]; + } + + readSubjectsMetas(dbCon, SMSubjectType.user, filter.getUserIdMask(), result); + StringBuilder teamsSql = new StringBuilder() + .append("SELECT USER_ID,TEAM_ID FROM {table_prefix}CB_USER_TEAM") + .append("\n") + .append("WHERE USER_ID IN (") + .append(SQLUtils.generateParamList(result.size())) + .append(")"); // Read teams - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID,TEAM_ID FROM {table_prefix}CB_USER_TEAM" + - (CommonUtils.isEmpty(userNameMask) ? "" : " WHERE USER_ID=?"))) - ) { - if (!CommonUtils.isEmpty(userNameMask)) { - dbStat.setString(1, userNameMask); + try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(teamsSql.toString()))) { + int parameterIndex = 1; + for (String userId : result.keySet()) { + dbStat.setString(parameterIndex++, userId); } try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { @@ -352,6 +388,33 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen } } + private String buildUsersFilter(SMUserFilter filter) { + StringBuilder where = new StringBuilder(); + List whereParts = new ArrayList<>(); + if (!CommonUtils.isEmpty(filter.getUserIdMask())) { + whereParts.add("USER_ID LIKE ?"); + } + if (filter.getEnabledState() != null) { + whereParts.add("IS_ACTIVE=?"); + } + if (whereParts.size() > 0) { + where.append(whereParts.stream().collect(Collectors.joining(" AND ", " WHERE ", ""))); + } + return where.toString(); + } + + private int setUsersFilterValues(PreparedStatement dbStat, SMUserFilter filter, int parameterIndex) + throws SQLException { + if (!CommonUtils.isEmpty(filter.getUserIdMask())) { + dbStat.setString(parameterIndex++, "%" + filter.getUserIdMask() + "%"); + } + if (filter.getEnabledState() != null) { + dbStat.setString(parameterIndex++, filter.getEnabledState() ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); + } + + return parameterIndex; + } + private void cleanupSubjectMeta(Connection dbCon, String subjectId) throws SQLException { // Delete old metas try (PreparedStatement dbStat = dbCon.prepareStatement( @@ -379,17 +442,18 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen } } - private void readSubjectsMetas(Connection dbCon, SMSubjectType subjectType, String nameMask, Map result) throws SQLException { + private void readSubjectsMetas(Connection dbCon, SMSubjectType subjectType, String userIdMask, + Map result) throws SQLException { // Read metas try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames("SELECT m.SUBJECT_ID,m.META_ID,m.META_VALUE FROM {table_prefix}CB_AUTH_SUBJECT s, " + "{table_prefix}CB_SUBJECT_META m\n" + "WHERE s.SUBJECT_TYPE=? AND s.SUBJECT_ID=m.SUBJECT_ID" + - (CommonUtils.isEmpty(nameMask) ? "" : " AND s.SUBJECT_ID=?"))) + (CommonUtils.isEmpty(userIdMask) ? "" : " AND s.SUBJECT_ID LIKE ?"))) ) { dbStat.setString(1, subjectType.getCode()); - if (!CommonUtils.isEmpty(nameMask)) { - dbStat.setString(2, nameMask); + if (!CommonUtils.isEmpty(userIdMask)) { + dbStat.setString(2, "%" + userIdMask + "%"); } try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index 6637397b01..9ad9bfdd0c 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -5,8 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { runInAction } from 'mobx'; - import { injectable } from '@cloudbeaver/core-di'; import { ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; import { @@ -14,13 +12,17 @@ import { AdminUserInfo, AdminUserInfoFragment, CachedMapAllKey, + CachedMapPageKey, CachedMapResource, GetUsersListQueryVariables, GraphQLService, + ICachedMapPageOptions, isResourceAlias, - isResourceKeyAlias, ResourceKey, + ResourceKeyFlat, resourceKeyList, + resourceKeyListAlias, + resourceKeyListAliasFactory, ResourceKeySimple, ResourceKeyUtils, } from '@cloudbeaver/core-sdk'; @@ -36,7 +38,20 @@ const NEW_USER_SYMBOL = Symbol('new-user'); export type AdminUser = AdminUserInfoFragment; type AdminUserNew = AdminUser & { [NEW_USER_SYMBOL]: boolean }; -type UserResourceIncludes = Omit; +type UserResourceIncludes = Omit; + +interface IUserResourceSearchPageOptions extends ICachedMapPageOptions { + userId?: string; + enabledState?: boolean; +} + +export const UsersResourceSearchUser = resourceKeyListAliasFactory< + any, + [offset: number, limit: number, userId?: string, enabledState?: boolean], + Readonly +>('@users-resource/page', (offset: number, limit: number, userId?: string, enabledState?: boolean) => ({ offset, limit, userId, enabledState })); + +export const UsersResourceNewUsers = resourceKeyListAlias('@users-resource/new-users'); interface UserCreateOptions { userId: string; @@ -60,13 +75,19 @@ export class UsersResource extends CachedMapResource { + const pageInfo = this.getPageInfo(key as ResourceKeyFlat); - isNew(id: string): boolean { - if (!this.has(id)) { - return true; - } - return NEW_USER_SYMBOL in this.get(id)!; + return resourceKeyList(pageInfo?.edges || []); + }); + + this.addAlias(UsersResourceNewUsers, () => { + const orderedKeys = this.entries + .filter(k => isNewUser(k[1])) + .sort((a, b) => compareUsers(a[1], b[1])) + .map(([key]) => key); + return resourceKeyList(orderedKeys); + }); } getEmptyUser(): AdminUserInfo { @@ -130,6 +151,12 @@ export class UsersResource extends CachedMapResource { await this.graphQLService.sdk.grantUserTeam({ userId, teamId }); @@ -211,7 +238,14 @@ export class UsersResource extends CachedMapResource, includes?: string[]): Promise> { + const search = this.isAlias(originalKey, UsersResourceSearchUser); + const page = this.isAlias(originalKey, CachedMapPageKey); const all = this.isAlias(originalKey, CachedMapAllKey); + + if (all) { + throw new Error('Loading all users is prohibited'); + } + const usersList: AdminUser[] = []; await ResourceKeyUtils.forEachAsync(originalKey, async key => { @@ -221,21 +255,41 @@ export class UsersResource extends CachedMapResource user.userId), + hasNextPage: users.length === originalKey.options.limit, + }); + } + } }); const key = resourceKeyList(usersList.map(user => user.userId)); - if (all) { - this.replace(key, usersList); - } else { - this.set(key, usersList); - } + this.set(key, usersList); return this.data; } @@ -247,6 +301,11 @@ export class UsersResource extends CachedMapResource origin.type === AUTH_PROVIDER_LOCAL_ID); } + +export function isNewUser(user: AdminUser): boolean { + return NEW_USER_SYMBOL in user; +} + +export function compareUsers(a: AdminUser, b: AdminUser): number { + return a.userId.localeCompare(b.userId); +} diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/usePagination.ts b/webapp/packages/core-blocks/src/ResourcesHooks/usePagination.ts new file mode 100644 index 0000000000..d325a4d73a --- /dev/null +++ b/webapp/packages/core-blocks/src/ResourcesHooks/usePagination.ts @@ -0,0 +1,112 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, computed, observable } from 'mobx'; + +import { type IServiceConstructor, useService } from '@cloudbeaver/core-di'; +import { + CachedMapPageKey, + CachedMapResource, + CachedMapResourceListGetter, + CachedMapResourceValue, + ICachedMapPageOptions, + ResourceKeyListAlias, +} from '@cloudbeaver/core-sdk'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; + +import { useObservableRef } from '../useObservableRef'; + +interface IOptions { + pageSize?: number; + getKey?: (offset: number, limit: number) => ResourceKeyListAlias>; + dependencies?: any[]; +} + +interface IPagination { + offset: number; + key: ResourceKeyListAlias>; + data: CachedMapResourceListGetter, []>; + hasNextPage: boolean; + loaded: ResourceKeyListAlias[]; + refresh: () => void; + loadMore: () => void; +} + +interface IPaginationPrivate extends IPagination { + pageSize: number; + resource: CachedMapResource; + dependencies: any[]; + reset(): void; +} + +const DEFAULT_PAGE_SIZE = 100; + +export function usePagination>( + ctor: IServiceConstructor, + options?: IOptions, +): IPagination { + const pageSize = options?.pageSize || DEFAULT_PAGE_SIZE; + const resource = useService(ctor); + + const pagination = useObservableRef>( + () => { + const key = options?.getKey?.(0, pageSize) || CachedMapPageKey(0, pageSize); + const dependencies = options?.dependencies || []; + + return { + offset: 0, + key, + dependencies, + get hasNextPage(): boolean { + return this.resource.getPageInfo(this.key)?.hasNextPage || false; + }, + get data(): CachedMapResourceListGetter, []> { + return this.loaded.map(key => this.resource.get(key)).flat(); + }, + loaded: [key], + loadMore() { + const pageInfo = this.resource.getPageInfo(this.key); + + if (pageInfo?.hasNextPage) { + this.offset = this.key.options.offset + this.pageSize; + this.key = options?.getKey?.(this.offset, this.pageSize) || CachedMapPageKey(this.offset, this.pageSize); + this.loaded.push(this.key); + } + }, + refresh() { + for (const key of this.loaded) { + this.resource.markOutdated(key); + } + this.reset(); + }, + reset() { + this.offset = 0; + this.key = options?.getKey?.(this.offset, this.pageSize) || CachedMapPageKey(this.offset, this.pageSize); + this.loaded = [this.key]; + }, + }; + }, + { + offset: observable.ref, + key: observable.ref, + loaded: observable.shallow, + hasNextPage: computed, + loadMore: action.bound, + reset: action.bound, + refresh: action.bound, + }, + { pageSize, resource }, + ); + + if (!isArraysEqual(pagination.dependencies, options?.dependencies || [])) { + pagination.dependencies = options?.dependencies || []; + + pagination.reset(); + } + + return pagination; +} diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index 0c1f9a3fec..6b8b0f930d 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -32,7 +32,7 @@ import { getComputed } from '../getComputed'; import { useObjectRef } from '../useObjectRef'; import { useObservableRef } from '../useObservableRef'; -interface KeyWithIncludes { +export interface ResourceKeyWithIncludes { readonly key: TKey | null; readonly includes: TIncludes; } @@ -66,8 +66,8 @@ interface IMapResourceState extends ILoadableState { loaded: boolean; resource: TResource; isOutdated: () => boolean; - load: () => void; - reload: () => void; + load: () => Promise; + reload: () => Promise; } interface IMapResourceListResult extends IMapResourceState { @@ -115,7 +115,7 @@ export function useResource< >( component: { name: string }, ctor: IServiceConstructor | TResource, - keyObj: TResource extends any ? TKeyArg | null | KeyWithIncludes : never, + keyObj: TResource extends any ? TKeyArg | null | ResourceKeyWithIncludes : never, actions?: TResource extends any ? IActions : never, ): TResult; @@ -126,7 +126,7 @@ export function useResource< >( component: { name: string }, ctor: IServiceConstructor | TResource, - keyObj: TResource extends any ? TKeyArg | null | KeyWithIncludes : never, + keyObj: TResource extends any ? TKeyArg | null | ResourceKeyWithIncludes : never, actions?: TResource extends any ? IActions : never, ): IMapResourceResult | IMapResourceListResult | IDataResourceResult { // eslint-disable-next-line react-hooks/rules-of-hooks @@ -462,6 +462,6 @@ export function useResource< return result; } -function isKeyWithIncludes(obj: any): obj is KeyWithIncludes { +function isKeyWithIncludes(obj: any): obj is ResourceKeyWithIncludes { return obj && typeof obj === 'object' && 'includes' in obj && 'key' in obj; } diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 636767d834..ea80310a87 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -179,6 +179,7 @@ export * from './useExecutor'; export * from './useFn'; export * from './useFocus'; export * from './useFormValidator'; +export * from './ResourcesHooks/usePagination'; export * from './ResourcesHooks/useResource'; export * from './useMouse'; export * from './useObjectRef'; diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 8b3e67e2a9..f73b888eaa 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -7,6 +7,7 @@ export default [ ['ui_stepper_back', 'Back'], ['ui_stepper_next', 'Next'], ['ui_stepper_finish', 'Finish'], + ['ui_load_more', 'Load more'], ['ui_processing_loading', 'Loading...'], ['ui_processing_cancel', 'Cancel'], ['ui_processing_canceling', 'Cancelling...'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index be26507731..e72ea858cd 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -7,6 +7,7 @@ export default [ ['ui_stepper_back', 'Indietro'], ['ui_stepper_next', 'Avanti'], ['ui_stepper_finish', 'Termina'], + ['ui_load_more', 'Load more'], ['ui_processing_loading', 'Caricamento...'], ['ui_processing_cancel', 'Annulla'], ['ui_processing_canceling', 'Annullamento...'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index f1e1249ae2..c421686d28 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -5,6 +5,7 @@ export default [ ['ui_light_theme', 'Светлая'], ['ui_dark_theme', 'Темная'], ['ui_stepper_back', 'Назад'], + ['ui_load_more', 'Загрузить ещё'], ['ui_processing_loading', 'Загрузка...'], ['ui_processing_cancel', 'Отменить'], ['ui_processing_canceling', 'Отмена...'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index e3d8f61b24..706d8ce268 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -7,6 +7,7 @@ export default [ ['ui_stepper_back', '返回'], ['ui_stepper_next', '下一步'], ['ui_stepper_finish', '完成'], + ['ui_load_more', 'Load more'], ['ui_processing_loading', '加载中...'], ['ui_processing_cancel', '取消'], ['ui_processing_canceling', '取消中...'], diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index 2b4e7dc932..a9b1f6e4a8 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -12,10 +12,10 @@ import { ILoadableState, isArraysEqual, isContainsException } from '@cloudbeaver import { CachedResource, CachedResourceKey, ICachedResourceMetadata } from './CachedResource'; import type { CachedResourceIncludeArgs, CachedResourceValueIncludes } from './CachedResourceIncludes'; -import type { ResourceKey, ResourceKeySimple } from './ResourceKey'; +import type { ResourceKey, ResourceKeyFlat, ResourceKeySimple } from './ResourceKey'; import type { ResourceKeyAlias } from './ResourceKeyAlias'; import { isResourceKeyList, resourceKeyList, ResourceKeyList } from './ResourceKeyList'; -import { ResourceKeyListAlias, resourceKeyListAlias } from './ResourceKeyListAlias'; +import { ResourceKeyListAlias, resourceKeyListAlias, resourceKeyListAliasFactory } from './ResourceKeyListAlias'; import { ResourceKeyUtils } from './ResourceKeyUtils'; export type CachedMapResourceKey = CachedResourceKey; @@ -30,8 +30,25 @@ export type CachedMapResourceLoader = TRealKe ? Array> : CachedResourceValueIncludes; +export interface ICachedMapPageOptions { + offset: number; + limit: number; +} + export const CachedMapAllKey = resourceKeyListAlias('@cached-map-resource/all'); export const CachedMapEmptyKey = resourceKeyListAlias('@cached-map-resource/empty'); +export const CachedMapPageKey = resourceKeyListAliasFactory>( + '@cached-map-resource/page', + (offset: number, limit: number) => ({ offset, limit }), +); + +interface IPageInfo { + totalCount?: number; + edges?: TKey[]; + hasNextPage?: boolean; +} + +type ICachedMapResourceMetadata = ICachedResourceMetadata & IPageInfo; /** * CachedMapResource is a resource that stores data in a Map. @@ -40,7 +57,7 @@ export abstract class CachedMapResource< TKey, TValue, TContext extends Record = Record, - TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata, + TMetadata extends ICachedMapResourceMetadata = ICachedMapResourceMetadata, > extends CachedResource, TValue, TKey, CachedResourceIncludeArgs, TMetadata> { readonly onItemUpdate: ISyncExecutor>; readonly onItemDelete: ISyncExecutor>; @@ -63,6 +80,11 @@ export abstract class CachedMapResource< this.onItemDelete = new SyncExecutor>(null); this.addAlias(CachedMapAllKey, () => resourceKeyList(this.keys)); + this.addAlias(CachedMapPageKey, key => { + const pageInfo = this.getPageInfo(key as ResourceKeyFlat); + + return resourceKeyList(pageInfo?.edges || []); + }); makeObservable(this, { set: action, @@ -121,6 +143,14 @@ export abstract class CachedMapResource< return ResourceKeyUtils.map(key, key => this.dataGet(this.getKeyRef(key))); } + getPageInfo(key: ResourceKeyFlat): IPageInfo | undefined { + if (!this.hasMetadata(key)) { + return undefined; + } + + return this.getMetadata(key); + } + set(key: TKey | ResourceKeyAlias, value: TValue): void; set(key: ResourceKeyList | ResourceKeyListAlias, value: TValue[]): void; set(key: ResourceKey, value: TValue | TValue[]): void; @@ -225,6 +255,14 @@ export abstract class CachedMapResource< return super.getKeyRef(key); } + protected setPageInfo(key: ResourceKeyFlat, pageInfo: IPageInfo): void { + const metadata = this.getMetadata(key); + + metadata.totalCount = pageInfo.totalCount; + metadata.edges = pageInfo.edges; + metadata.hasNextPage = pageInfo.hasNextPage; + } + /** * Use it instead of this.data.has * This method can be override diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index b3b6094337..5e85460760 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -100,6 +100,7 @@ export abstract class CachedResource< /** Need to infer value type */ private readonly typescriptHack: TValue; + private captureAliasGetterExecution: boolean; constructor(defaultKey: ResourceKey, private readonly defaultValue: () => TData, defaultIncludes: TInclude = [] as any) { super(); @@ -114,6 +115,7 @@ export abstract class CachedResource< this.defaultIncludes = defaultIncludes; this.paramAliases = []; this.outdateWaitList = []; + this.captureAliasGetterExecution = false; this.metadata = new MetadataMap((key, metadata) => observable(this.getDefaultMetadata(key, metadata) as TMetadata, undefined, { deep: false })); this.scheduler = new TaskScheduler(this.isIntersect); this.data = defaultValue(); @@ -824,7 +826,7 @@ export abstract class CachedResource< if (isResourceAlias(param)) { for (const alias of this.paramAliases) { if (alias.id === param.id) { - param = alias.getAlias(param); + param = this.captureGetAlias(alias, param); deep++; break; } @@ -848,6 +850,10 @@ export abstract class CachedResource< protected transformToAlias( key: ResourceKeyAlias | ResourceKeyListAlias, ): ResourceKeyAlias | ResourceKeyListAlias { + if (this.captureAliasGetterExecution) { + return key; + } + let deep = 0; // eslint-disable-next-line no-labels transform: if (deep < 10) { @@ -862,7 +868,7 @@ export abstract class CachedResource< for (const alias of this.paramAliases) { if (alias.id === key.id) { - const data = alias.getAlias(key); + const data = this.captureGetAlias(alias, key); if (isResourceAlias(data)) { key = data; @@ -880,6 +886,15 @@ export abstract class CachedResource< return key; } + protected captureGetAlias(alias: IParamAlias, param: ResourceAlias): ResourceKey { + try { + this.captureAliasGetterExecution = true; + return alias.getAlias(param); + } finally { + this.captureAliasGetterExecution = false; + } + } + /** * Check if key is valid. Can be overridden to provide custom validation. * When key is alias checks that alias is registered. diff --git a/webapp/packages/core-sdk/src/Resource/ResourceAlias.ts b/webapp/packages/core-sdk/src/Resource/ResourceAlias.ts index 399289f6df..00e4be1f70 100644 --- a/webapp/packages/core-sdk/src/Resource/ResourceAlias.ts +++ b/webapp/packages/core-sdk/src/Resource/ResourceAlias.ts @@ -8,7 +8,8 @@ import { isObjectsEqual } from '@cloudbeaver/core-utils'; export type ResourceAliasOptionsKey = string | number; -export type ResourceAliasOptionsValue = ResourceAliasOptionsKey | Array; +export type ResourceAliasOptionsValueTypes = string | number | boolean | null | undefined; +export type ResourceAliasOptionsValue = ResourceAliasOptionsValueTypes | Array; export type ResourceAliasOptions = Readonly> | undefined; export abstract class ResourceAlias { diff --git a/webapp/packages/core-sdk/src/queries/authentication/users/getUserById.gql b/webapp/packages/core-sdk/src/queries/authentication/users/getUserById.gql new file mode 100644 index 0000000000..b4ff8c390e --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/users/getUserById.gql @@ -0,0 +1,5 @@ +query getAdminUserInfo($userId: ID!, $includeMetaParameters: Boolean!, $customIncludeOriginDetails: Boolean!) { + user: adminUserInfo(userId: $userId) { + ...AdminUserInfo + } +} diff --git a/webapp/packages/core-sdk/src/queries/authentication/users/getUsersList.gql b/webapp/packages/core-sdk/src/queries/authentication/users/getUsersList.gql index 31cfb34d7a..fd0f1c5eeb 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/users/getUsersList.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/users/getUsersList.gql @@ -1,5 +1,5 @@ -query getUsersList($userId: ID, $includeMetaParameters: Boolean!, $customIncludeOriginDetails: Boolean!) { - users: listUsers(userId: $userId) { +query getUsersList($page: PageInput!, $filter: AdminUserFilterInput!, $includeMetaParameters: Boolean!, $customIncludeOriginDetails: Boolean!) { + users: listUsers(page: $page, filter: $filter) { ...AdminUserInfo - } -} \ No newline at end of file + } +} diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 41a416c15e..384bf367b6 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -82,6 +82,11 @@ export interface AdminTeamInfo { teamPermissions: Array; } +export interface AdminUserFilterInput { + enabledState?: InputMaybe; + userIdMask?: InputMaybe; +} + export interface AdminUserInfo { authRole?: Maybe; configurationParameters: Scalars['Object']; @@ -933,6 +938,11 @@ export enum ObjectPropertyLength { Tiny = 'TINY', } +export interface PageInput { + limit?: InputMaybe; + offset?: InputMaybe; +} + export interface ProductInfo { buildTime: Scalars['String']; description?: Maybe; @@ -959,6 +969,7 @@ export interface ProjectInfo { export interface Query { activeUser?: Maybe; + adminUserInfo: AdminUserInfo; authChangeLocalPassword: Scalars['Boolean']; authLogin: AuthInfo; authLogout?: Maybe; @@ -1043,6 +1054,10 @@ export interface Query { userConnections: Array; } +export interface QueryAdminUserInfoArgs { + userId: Scalars['ID']; +} + export interface QueryAuthChangeLocalPasswordArgs { newPassword: Scalars['String']; oldPassword: Scalars['String']; @@ -1167,7 +1182,8 @@ export interface QueryListTeamsArgs { } export interface QueryListUsersArgs { - userId?: InputMaybe; + filter: AdminUserFilterInput; + page: PageInput; } export interface QueryMetadataGetNodeDdlArgs { @@ -2124,6 +2140,42 @@ export type EnableUserQueryVariables = Exact<{ export type EnableUserQuery = { enableUser?: boolean }; +export type GetAdminUserInfoQueryVariables = Exact<{ + userId: Scalars['ID']; + includeMetaParameters: Scalars['Boolean']; + customIncludeOriginDetails: Scalars['Boolean']; +}>; + +export type GetAdminUserInfoQuery = { + user: { + userId: string; + grantedTeams: Array; + linkedAuthProviders: Array; + metaParameters?: any; + enabled: boolean; + authRole?: string; + origins: Array<{ + type: string; + subType?: string; + displayName: string; + icon?: string; + details?: Array<{ + id?: string; + displayName?: string; + description?: string; + category?: string; + dataType?: string; + defaultValue?: any; + validValues?: Array; + value?: any; + length: ObjectPropertyLength; + features: Array; + order: number; + }>; + }>; + }; +}; + export type GetUserGrantedConnectionsQueryVariables = Exact<{ userId: Scalars['ID']; }>; @@ -2133,7 +2185,8 @@ export type GetUserGrantedConnectionsQuery = { }; export type GetUsersListQueryVariables = Exact<{ - userId?: InputMaybe; + page: PageInput; + filter: AdminUserFilterInput; includeMetaParameters: Scalars['Boolean']; customIncludeOriginDetails: Scalars['Boolean']; }>; @@ -5518,6 +5571,13 @@ export const EnableUserDocument = ` enableUser(userId: $userId, enabled: $enabled) } `; +export const GetAdminUserInfoDocument = ` + query getAdminUserInfo($userId: ID!, $includeMetaParameters: Boolean!, $customIncludeOriginDetails: Boolean!) { + user: adminUserInfo(userId: $userId) { + ...AdminUserInfo + } +} + ${AdminUserInfoFragmentDoc}`; export const GetUserGrantedConnectionsDocument = ` query getUserGrantedConnections($userId: ID!) { grantedConnections: getSubjectConnectionAccess(subjectId: $userId) { @@ -5529,8 +5589,8 @@ export const GetUserGrantedConnectionsDocument = ` } `; export const GetUsersListDocument = ` - query getUsersList($userId: ID, $includeMetaParameters: Boolean!, $customIncludeOriginDetails: Boolean!) { - users: listUsers(userId: $userId) { + query getUsersList($page: PageInput!, $filter: AdminUserFilterInput!, $includeMetaParameters: Boolean!, $customIncludeOriginDetails: Boolean!) { + users: listUsers(page: $page, filter: $filter) { ...AdminUserInfo } } @@ -6884,6 +6944,14 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = 'query', ); }, + getAdminUserInfo(variables: GetAdminUserInfoQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper( + wrappedRequestHeaders => + client.request(GetAdminUserInfoDocument, variables, { ...requestHeaders, ...wrappedRequestHeaders }), + 'getAdminUserInfo', + 'query', + ); + }, getUserGrantedConnections( variables: GetUserGrantedConnectionsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/MetaParameters/MetaParametersController.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/MetaParameters/MetaParametersController.ts index bd0157d4bd..0838d36a8b 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/MetaParameters/MetaParametersController.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/MetaParameters/MetaParametersController.ts @@ -7,7 +7,7 @@ */ import { computed, makeObservable, observable } from 'mobx'; -import { AdminUser, AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { AdminUser, AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, compareUsers, UsersResource } from '@cloudbeaver/core-authentication'; import { IInitializableController, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -23,15 +23,7 @@ export class MetaParametersController implements IInitializableController { readonly error = new GQLErrorCatcher(); get users(): AdminUser[] { - return Array.from(this.usersResource.data.values()).sort((a, b) => { - if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) { - return a.userId.localeCompare(b.userId); - } - if (this.usersResource.isNew(a.userId)) { - return -1; - } - return 1; - }); + return Array.from(this.usersResource.data.values()).sort(compareUsers); } get isProvidersLoading(): boolean { diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx index 3b18cbf106..91076d29d1 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { AdminUser, UsersResource } from '@cloudbeaver/core-authentication'; +import { AdminUser, UsersResource, UsersResourceSearchUser } from '@cloudbeaver/core-authentication'; import { ColoredContainer, Container, @@ -22,7 +22,6 @@ import { useStyles, useTranslate, } from '@cloudbeaver/core-blocks'; -import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import type { ITeamFormProps } from '../ITeamFormProps'; @@ -53,7 +52,7 @@ export const GrantedUsers: TabContainerPanelComponent = observer const state = useGrantedUsers(formState.config, formState.mode); const { selected } = useTab(tabId); - const users = useResource(GrantedUsers, UsersResource, CachedMapAllKey, { active: selected }); + const users = useResource(GrantedUsers, UsersResource, UsersResourceSearchUser(0, 1000), { active: selected }); const grantedUsers = getComputed(() => users.data.filter((user): user is AdminUser => !!user && state.state.grantedUsers.includes(user.userId)), diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserFormController.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserFormController.ts index bac68cbd4c..04f7838006 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserFormController.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserFormController.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { computed, makeObservable, observable } from 'mobx'; +import { computed, makeObservable, observable, toJS } from 'mobx'; import { AdminUser, AuthRolesResource, compareTeams, isLocalUser, TeamInfo, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; import { @@ -359,7 +359,7 @@ export class UserFormController implements IInitializableController, IDestructib private async loadUser() { try { - this.credentials.metaParameters = this.user.metaParameters; + this.credentials.metaParameters = observable(toJS(this.user.metaParameters ?? {})); this.credentials.login = this.user.userId; this.credentials.teams = new Map(this.user.grantedTeams.map(teamId => [teamId, true])); this.credentials.authRole = this.user.authRole; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts index e59319f154..1110105765 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts @@ -8,7 +8,7 @@ import React from 'react'; import { AdministrationItemService } from '@cloudbeaver/core-administration'; -import { AdminUser, TeamsResource } from '@cloudbeaver/core-authentication'; +import { AdminUser, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; @@ -44,6 +44,7 @@ export class UsersAdministrationService extends Bootstrap { private readonly createUserService: CreateUserService, private readonly teamsResource: TeamsResource, private readonly createTeamService: CreateTeamService, + private readonly usersResource: UsersResource, ) { super(); } @@ -63,7 +64,7 @@ export class UsersAdministrationService extends Bootstrap { { name: EUsersAdministrationSub.Teams, onActivate: this.loadTeams.bind(this), - onDeActivate: (configurationWizard, outside) => { + onDeActivate: (param, configurationWizard, outside) => { if (outside) { this.teamsResource.cleanNewFlags(); } @@ -79,10 +80,14 @@ export class UsersAdministrationService extends Bootstrap { load(): void | Promise {} - private async cancelCreate(param: string | null) { + private async cancelCreate(param: string | null, configurationWizard: boolean, outside: boolean) { if (param === 'create') { this.createUserService.close(); } + + if (outside) { + this.usersResource.cleanNewFlags(); + } } private async loadTeams(param: string | null) { diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx index 5caf98418f..ad542d901e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx @@ -5,22 +5,17 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, computed, observable } from 'mobx'; +import { action, observable } from 'mobx'; import type { AdminUser } from '@cloudbeaver/core-authentication'; import { useObservableRef } from '@cloudbeaver/core-blocks'; import type { TLocalizationToken } from '@cloudbeaver/core-localization'; -import { isArraysEqual } from '@cloudbeaver/core-utils'; -export enum EUserStatus { - ENABLED = 'ENABLED', - DISABLED = 'DISABLED', - ALL = 'ALL', -} +type EnabledStatusType = 'true' | 'false' | 'all'; interface IStatus { label: TLocalizationToken; - value: EUserStatus; + value: EnabledStatusType; } export const USER_ROLE_ALL = 'ALL'; @@ -28,38 +23,42 @@ export const USER_ROLE_ALL = 'ALL'; export const USER_STATUSES: IStatus[] = [ { label: 'authentication_administration_users_filters_status_enabled', - value: EUserStatus.ENABLED, + value: 'true', }, { label: 'authentication_administration_users_filters_status_disabled', - value: EUserStatus.DISABLED, + value: 'false', }, { label: 'authentication_administration_users_filters_status_all', - value: EUserStatus.ALL, + value: 'all', }, ]; export interface IUserFilters { search: string; role: string; - status: EUserStatus; - filteredUsers: AdminUser[]; + status: EnabledStatusType; + isSearching: boolean; + filterUsers: (users: AdminUser[]) => AdminUser[]; setSearch: (value: string) => void; setRole: (role: string) => void; - setStatus: (status: EUserStatus) => void; + setStatus: (status: EnabledStatusType) => void; } -export function useUsersTableFilters(users: AdminUser[]) { +export function useUsersTableFilters() { const filters: IUserFilters = useObservableRef( () => ({ search: '', role: USER_ROLE_ALL, - status: EUserStatus.ENABLED, - get filteredUsers() { - return this.users.filter(user => { + status: 'true', + get isSearching() { + return this.search.trim() !== ''; + }, + filterUsers(users: AdminUser[]) { + return users.filter(user => { const matchSearch = user.userId.toLowerCase().includes(this.search.trim().toLowerCase()); - const matchStatus = this.status === EUserStatus.ALL || (this.status === EUserStatus.ENABLED ? user.enabled : !user.enabled); + const matchStatus = String(user.enabled) === this.status || this.status === 'all'; const matchRole = this.role === USER_ROLE_ALL || this.role === user.authRole; return matchSearch && matchStatus && matchRole; @@ -71,7 +70,7 @@ export function useUsersTableFilters(users: AdminUser[]) { setRole(role: string) { this.role = role; }, - setStatus(status: EUserStatus) { + setStatus(status: EnabledStatusType) { this.status = status; }, }), @@ -79,13 +78,11 @@ export function useUsersTableFilters(users: AdminUser[]) { search: observable.ref, role: observable.ref, status: observable.ref, - users: observable.ref, - filteredUsers: computed({ equals: (first, second) => isArraysEqual(first, second, undefined, true) }), setSearch: action.bound, setRole: action.bound, setStatus: action.bound, }, - { users }, + false, ); return filters; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx index 261265c840..542e34831a 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx @@ -9,11 +9,11 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useContext } from 'react'; import styled, { css } from 'reshadow'; -import { Loader, TableContext } from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { Loader, TableContext, useResource } from '@cloudbeaver/core-blocks'; +import type { AdminUserInfo } from '@cloudbeaver/core-sdk'; import { UserForm } from '../UserForm/UserForm'; -import { UserEditController } from './UserEditController'; const styles = css` box { @@ -31,13 +31,9 @@ interface Props { } export const UserEdit = observer(function UserEdit({ item }) { - const controller = useController(UserEditController, item); + const user = useResource(UserEdit, UsersResource, { key: item, includes: ['includeMetaParameters'] }); const tableContext = useContext(TableContext); const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext]); - return styled(styles)( - - {controller.user ? : } - , - ); + return styled(styles)({user.data ? : }); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEditController.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEditController.ts deleted file mode 100644 index 4e391c620e..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEditController.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { computed, makeObservable, observable } from 'mobx'; - -import { UsersResource } from '@cloudbeaver/core-authentication'; -import { IDestructibleController, IInitializableController, injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { AdminUserInfo, GQLErrorCatcher, ResourceKey, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; - -@injectable() -export class UserEditController implements IInitializableController, IDestructibleController { - isLoading = true; - user: AdminUserInfo | null = null; - - get isDisabled() { - return this.usersResource.isLoading(this.userId); - } - - userId!: string; - - readonly error = new GQLErrorCatcher(); - - constructor(private readonly notificationService: NotificationService, private readonly usersResource: UsersResource) { - makeObservable(this, { - isLoading: observable, - user: observable, - isDisabled: computed, - }); - - this.updateUser = this.updateUser.bind(this); - } - - async init(id: string): Promise { - this.userId = id; - - await this.loadUser(); - this.usersResource.onItemUpdate.addHandler(this.updateUser); - } - - destruct(): void { - this.usersResource.onItemUpdate.removeHandler(this.updateUser); - } - - private async loadUser() { - try { - // we create a copy to protect the current value from mutation - await this.usersResource.load(this.userId, ['includeMetaParameters']); - await this.updateUser(this.userId); - } catch (exception: any) { - this.notificationService.logException(exception, `Can't load user ${this.userId}`); - } - } - - private async updateUser(key: ResourceKeySimple) { - if (!ResourceKeyUtils.isIntersect(key, this.userId)) { - return; - } - this.user = JSON.parse(JSON.stringify(await this.usersResource.load(this.userId, ['includeMetaParameters']))); - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx index 7e2fe6aec5..8fadf28480 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx @@ -9,8 +9,8 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration'; -import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, AuthRolesResource, UsersResource } from '@cloudbeaver/core-authentication'; -import { ColoredContainer, Container, Group, Loader, ToolsAction, ToolsPanel, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, AuthRolesResource } from '@cloudbeaver/core-authentication'; +import { ColoredContainer, Container, Group, ToolsAction, ToolsPanel, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; @@ -21,12 +21,6 @@ import { useUsersTableFilters } from './Filters/useUsersTableFilters'; import { UsersTable } from './UsersTable'; import { useUsersTable } from './useUsersTable'; -const loaderStyle = css` - ExceptionMessage { - padding: 24px; - } -`; - const styles = css` ToolsPanel { border-bottom: none; @@ -45,17 +39,16 @@ export const UsersPage = observer(function UsersPage({ sub, param }) { const createUserService = useService(CreateUserService); - const usersResource = useResource(UsersPage, UsersResource, CachedMapAllKey); const authProvidersResource = useResource(UsersPage, AuthProvidersResource, CachedMapAllKey); const authRolesResource = useResource(UsersPage, AuthRolesResource, undefined); - const table = useUsersTable(usersResource.resource); - const filters = useUsersTableFilters(table.users); + const filters = useUsersTableFilters(); + const table = useUsersTable(filters); const isLocalProviderAvailable = authProvidersResource.resource.has(AUTH_PROVIDER_LOCAL_ID); const create = param === 'create'; const displayAuthRole = authRolesResource.data.length > 0; - const keys = filters.filteredUsers.map(user => user.userId); + const loading = authRolesResource.isLoading() || table.loadableState.isLoading(); return styled(style)( @@ -105,15 +98,15 @@ export const UsersPage = observer(function UsersPage({ sub, param }) { )} - - - + , diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx index df337f534f..4509e0a164 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx @@ -7,21 +7,43 @@ */ import { observer } from 'mobx-react-lite'; -import { Table, TableBody, TableColumnHeader, TableHeader, useTranslate } from '@cloudbeaver/core-blocks'; +import { + Button, + Loader, + Table, + TableBody, + TableColumnHeader, + TableColumnValue, + TableHeader, + TableItem, + useTranslate, +} from '@cloudbeaver/core-blocks'; import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; import { User } from './User'; interface Props { - keys: string[]; users: AdminUserInfoFragment[]; selectedItems: Map; expandedItems: Map; displayAuthRole: boolean; + loading?: boolean; + hasMore: boolean; + onLoadMore?: () => void; } -export const UsersTable = observer(function UsersTable({ keys, users, selectedItems, expandedItems, displayAuthRole }) { +export const UsersTable = observer(function UsersTable({ + users, + selectedItems, + expandedItems, + displayAuthRole, + loading, + hasMore, + onLoadMore, +}) { const translate = useTranslate(); + const keys = users.map(user => user.userId); + const colSpan = displayAuthRole ? 6 : 5; return ( @@ -31,7 +53,9 @@ export const UsersTable = observer(function UsersTable({ keys, users, sel )} */} - + + + {translate('authentication_user_name')} {displayAuthRole && {translate('authentication_user_role')}} {translate('authentication_user_team')} @@ -47,6 +71,22 @@ export const UsersTable = observer(function UsersTable({ keys, users, sel // selectable={isLocalProviderAvailable} /> ))} + {(loading || users.length === 0) && ( + + + {translate(loading ? 'ui_processing_loading' : 'authentication_administration_users_empty')} + + + )} + {hasMore && ( + + + + + + )}
); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx index 163a8ec557..3b97033c2f 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx @@ -7,24 +7,42 @@ */ import { action, computed, observable } from 'mobx'; -import type { AdminUser, UsersResource } from '@cloudbeaver/core-authentication'; -import { TableState, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks'; +import { AdminUser, UsersResource, UsersResourceNewUsers, UsersResourceSearchUser } from '@cloudbeaver/core-authentication'; +import { TableState, useObservableRef, usePagination, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { resourceKeyList } from '@cloudbeaver/core-sdk'; -import { isArraysEqual } from '@cloudbeaver/core-utils'; +import { ILoadableState, isArraysEqual, isDefined } from '@cloudbeaver/core-utils'; + +import type { IUserFilters } from './Filters/useUsersTableFilters'; interface State { loading: boolean; + readonly hasMore: boolean; state: TableState; users: AdminUser[]; + loadableState: ILoadableState; + loadMore(): void; update: () => Promise; delete: () => Promise; } -export function useUsersTable(usersResource: UsersResource) { +export function useUsersTable(filters: IUserFilters) { const translate = useTranslate(); + const usersResource = useService(UsersResource); + const pagination = usePagination(UsersResource, { + getKey(offset, limit) { + return UsersResourceSearchUser( + offset, + limit, + filters.search, + filters.status === 'true' ? true : filters.status === 'false' ? false : undefined, + ); + }, + dependencies: [filters.search, filters.status], + }); + const usersLoader = useResource(useUsersTable, usersResource, pagination.key); const notificationService = useService(NotificationService); const commonDialogService = useService(CommonDialogService); @@ -32,20 +50,16 @@ export function useUsersTable(usersResource: UsersResource) { () => ({ loading: false, state: new TableState(), + get hasMore() { + return pagination.hasNextPage; + }, get users() { - return this.usersResource.values.slice().sort((a, b) => { - if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) { - return a.userId.localeCompare(b.userId); - } - if (this.usersResource.isNew(a.userId)) { - return -1; - } - return 1; - }); + const users = Array.from(new Set([UsersResourceNewUsers, ...pagination.loaded].map(key => this.usersLoader.resource.get(key)).flat())); + return filters.filterUsers(users.filter(isDefined)); }, async update() { try { - await this.usersResource.refreshAll(); + pagination.refresh(); notificationService.logSuccess({ title: 'authentication_administration_tools_refresh_success' }); } catch (exception: any) { notificationService.logException(exception, 'authentication_administration_tools_refresh_fail'); @@ -77,7 +91,7 @@ export function useUsersTable(usersResource: UsersResource) { this.loading = true; try { - await this.usersResource.delete(resourceKeyList(deletionList)); + await this.usersLoader.resource.delete(resourceKeyList(deletionList)); this.state.unselect(); for (const id of deletionList) { @@ -89,14 +103,18 @@ export function useUsersTable(usersResource: UsersResource) { this.loading = false; } }, + loadMore() { + pagination.loadMore(); + }, }), { loading: observable.ref, users: computed({ equals: (first, second) => isArraysEqual(first, second, undefined, true) }), update: action.bound, delete: action.bound, + loadMore: action.bound, }, - { usersResource }, + { usersLoader, loadableState: usersLoader }, ); return state; diff --git a/webapp/packages/plugin-authentication-administration/src/locales/en.ts b/webapp/packages/plugin-authentication-administration/src/locales/en.ts index 7459129171..a014f33909 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/en.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/en.ts @@ -30,6 +30,7 @@ export default [ ['authentication_administration_users_filters_status_enabled', 'ENABLED'], ['authentication_administration_users_filters_status_disabled', 'DISABLED'], ['authentication_administration_users_filters_status_all', 'ALL'], + ['authentication_administration_users_empty', 'There are no users'], ['authentication_administration_user_remove_credentials_error', 'Failed to remove user credentials'], ['authentication_administration_user_remove_credentials_success', 'User credentials were removed'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/it.ts b/webapp/packages/plugin-authentication-administration/src/locales/it.ts index c51a81be1c..4707a80a03 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/it.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/it.ts @@ -26,6 +26,7 @@ export default [ ['authentication_administration_users_filters_status_enabled', 'ENABLED'], ['authentication_administration_users_filters_status_disabled', 'DISABLED'], ['authentication_administration_users_filters_status_all', 'ALL'], + ['authentication_administration_users_empty', 'There are no users'], ['authentication_administration_user_remove_credentials_error', 'Failed to remove user credentials'], ['authentication_administration_user_remove_credentials_success', 'User credentials were removed'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/ru.ts b/webapp/packages/plugin-authentication-administration/src/locales/ru.ts index ca21e75022..9fdd304dd5 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/ru.ts @@ -30,6 +30,7 @@ export default [ ['authentication_administration_users_filters_status_enabled', 'ВКЛЮЧЕН'], ['authentication_administration_users_filters_status_disabled', 'ВЫКЛЮЧЕН'], ['authentication_administration_users_filters_status_all', 'ВСЕ'], + ['authentication_administration_users_empty', 'Нет пользователей'], ['authentication_administration_user_remove_credentials_error', 'Не удалось удалить учетные данные пользователя'], ['authentication_administration_user_remove_credentials_success', 'Учетные данные пользователя удалены'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/zh.ts b/webapp/packages/plugin-authentication-administration/src/locales/zh.ts index 239ac5048b..623c327e83 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/zh.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/zh.ts @@ -29,6 +29,7 @@ export default [ ['authentication_administration_users_filters_status_enabled', 'ENABLED'], ['authentication_administration_users_filters_status_disabled', 'DISABLED'], ['authentication_administration_users_filters_status_all', 'ALL'], + ['authentication_administration_users_empty', 'There are no users'], ['authentication_administration_user_remove_credentials_error', 'Failed to remove user credentials'], ['authentication_administration_user_remove_credentials_success', 'User credentials were removed'], diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx index 7966fea6da..30f82e3e63 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import styled, { css } from 'reshadow'; -import { TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { TeamsResource, UsersResource, UsersResourceSearchUser } from '@cloudbeaver/core-authentication'; import { ColoredContainer, Container, @@ -56,7 +56,7 @@ export const ConnectionAccess: TabContainerPanelComponent useAutoLoad(state, selected); - const users = useResource(ConnectionAccess, UsersResource, CachedMapAllKey, { active: selected }); + const users = useResource(ConnectionAccess, UsersResource, UsersResourceSearchUser(0, 1000), { active: selected }); const teams = useResource(ConnectionAccess, TeamsResource, CachedMapAllKey, { active: selected }); const grantedUsers = useMemo( diff --git a/webapp/packages/prettier-config/index.json b/webapp/packages/prettier-config/index.json index 20148473eb..4baea75ed8 100644 --- a/webapp/packages/prettier-config/index.json +++ b/webapp/packages/prettier-config/index.json @@ -8,9 +8,9 @@ "printWidth": 150, "arrowParens": "avoid", "plugins": ["@trivago/prettier-plugin-sort-imports"], - "importOrderParserPlugins" : ["typescript", "jsx", "decorators-legacy"], + "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], "importOrder": ["^@cloudbeaver/(.*)$", "^[./]"], "importOrderSeparation": true, "importOrderSortSpecifiers": true, "importOrderCaseInsensitive": true -} \ No newline at end of file +}