mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
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 <aleksandr.skoblikov@dbeaver.com> Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
co-authored by
Aleksandr Skoblikov
Daria Marutkina
parent
e33eb9fd60
commit
3cdcb07646
@@ -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<String, Object> params) {
|
||||
super(JSONUtils.getInteger(params, "offset"), JSONUtils.getInteger(params, "limit"));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@ scalar Object
|
||||
# Date/Time
|
||||
scalar DateTime
|
||||
|
||||
input PageInput {
|
||||
limit: Int
|
||||
offset: Int
|
||||
}
|
||||
|
||||
type Query
|
||||
|
||||
type Mutation
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
@@ -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!]!
|
||||
|
||||
+47
@@ -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<String, Object> params, @NotNull Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -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<AdminUserInfo> 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<AdminUserInfo> listUsers(@NotNull WebSession webSession, AdminUserInfoFilter filter)
|
||||
throws DBWebException;
|
||||
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
@NotNull
|
||||
|
||||
+13
-3
@@ -31,7 +31,8 @@ import org.jkiss.utils.CommonUtils;
|
||||
/**
|
||||
* Web service implementation
|
||||
*/
|
||||
public class WebServiceBindingAdmin extends WebServiceBindingBase<DBWServiceAdmin> implements DBWServiceBindingServlet<CBApplication> {
|
||||
public class WebServiceBindingAdmin extends WebServiceBindingBase<DBWServiceAdmin>
|
||||
implements DBWServiceBindingServlet<CBApplication> {
|
||||
|
||||
private static final String SCHEMA_FILE_NAME = "schema/service.admin.graphqls";
|
||||
|
||||
@@ -42,8 +43,17 @@ public class WebServiceBindingAdmin extends WebServiceBindingBase<DBWServiceAdmi
|
||||
@Override
|
||||
public void bindWiring(DBWBindingContext model) throws DBWebException {
|
||||
model.getQueryType()
|
||||
.dataFetcher("listUsers",
|
||||
env -> 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",
|
||||
|
||||
+15
-6
@@ -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<AdminUserInfo> listUsers(@NotNull WebSession webSession, String userName) throws DBWebException {
|
||||
public List<AdminUserInfo> listUsers(@NotNull WebSession webSession, @NotNull AdminUserInfoFilter adminFilter) throws DBWebException {
|
||||
try {
|
||||
List<AdminUserInfo> webUsers = new ArrayList<>();
|
||||
for (SMUser smUser : webSession.getAdminSecurityController().findUsers(userName)) {
|
||||
webUsers.add(new AdminUserInfo(webSession, new WebUser(smUser)));
|
||||
List<AdminUserInfo> 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);
|
||||
}
|
||||
|
||||
+81
-17
@@ -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<String, SMUser> 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<String> 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<String, ? extends SMSubject> result) throws SQLException {
|
||||
private void readSubjectsMetas(Connection dbCon, SMSubjectType subjectType, String userIdMask,
|
||||
Map<String, ? extends SMSubject> 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()) {
|
||||
|
||||
@@ -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<GetUsersListQueryVariables, 'userId'>;
|
||||
type UserResourceIncludes = Omit<GetUsersListQueryVariables, 'userId' | 'page' | 'filter'>;
|
||||
|
||||
interface IUserResourceSearchPageOptions extends ICachedMapPageOptions {
|
||||
userId?: string;
|
||||
enabledState?: boolean;
|
||||
}
|
||||
|
||||
export const UsersResourceSearchUser = resourceKeyListAliasFactory<
|
||||
any,
|
||||
[offset: number, limit: number, userId?: string, enabledState?: boolean],
|
||||
Readonly<IUserResourceSearchPageOptions>
|
||||
>('@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<string, AdminUser, UserReso
|
||||
super();
|
||||
|
||||
sessionPermissionsResource.require(this, EAdminPermission.admin).outdateResource(this);
|
||||
}
|
||||
this.addAlias(UsersResourceSearchUser, key => {
|
||||
const pageInfo = this.getPageInfo(key as ResourceKeyFlat<string>);
|
||||
|
||||
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<string, AdminUser, UserReso
|
||||
return this.get(user.userId)!;
|
||||
}
|
||||
|
||||
cleanNewFlags(): void {
|
||||
for (const user of this.data.values()) {
|
||||
(user as AdminUserNew)[NEW_USER_SYMBOL] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async grantTeam(userId: string, teamId: string, skipUpdate?: boolean): Promise<void> {
|
||||
await this.graphQLService.sdk.grantUserTeam({ userId, teamId });
|
||||
|
||||
@@ -211,7 +238,14 @@ export class UsersResource extends CachedMapResource<string, AdminUser, UserReso
|
||||
}
|
||||
|
||||
protected async loader(originalKey: ResourceKey<string>, includes?: string[]): Promise<Map<string, AdminUser>> {
|
||||
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<string, AdminUser, UserReso
|
||||
userId = key;
|
||||
}
|
||||
|
||||
const { users } = await this.graphQLService.sdk.getUsersList({
|
||||
userId,
|
||||
...this.getDefaultIncludes(),
|
||||
...this.getIncludesMap(userId, includes),
|
||||
});
|
||||
if (userId !== undefined) {
|
||||
const { user } = await this.graphQLService.sdk.getAdminUserInfo({
|
||||
userId,
|
||||
...this.getDefaultIncludes(),
|
||||
...this.getIncludesMap(userId, includes),
|
||||
});
|
||||
|
||||
usersList.push(...users);
|
||||
usersList.push(user);
|
||||
} else {
|
||||
const { users } = await this.graphQLService.sdk.getUsersList({
|
||||
page: {
|
||||
offset: page || search ? originalKey.options.offset : 100,
|
||||
limit: page || search ? originalKey.options.limit : 0,
|
||||
},
|
||||
filter: {
|
||||
userIdMask: search ? originalKey.options.userId : undefined,
|
||||
enabledState: search ? originalKey.options.enabledState : undefined,
|
||||
},
|
||||
...this.getDefaultIncludes(),
|
||||
...this.getIncludesMap(userId, includes),
|
||||
});
|
||||
|
||||
usersList.push(...users);
|
||||
|
||||
if (page || search) {
|
||||
this.setPageInfo(originalKey, {
|
||||
edges: users.map(user => 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<string, AdminUser, UserReso
|
||||
};
|
||||
}
|
||||
|
||||
protected dataSet(key: string, value: AdminUserInfoFragment): void {
|
||||
const oldValue = this.data.get(key);
|
||||
super.dataSet(key, { ...oldValue, ...value });
|
||||
}
|
||||
|
||||
protected validateKey(key: string): boolean {
|
||||
return typeof key === 'string';
|
||||
}
|
||||
@@ -255,3 +314,11 @@ export class UsersResource extends CachedMapResource<string, AdminUser, UserReso
|
||||
export function isLocalUser(user: AdminUser): boolean {
|
||||
return user.origins.some(origin => 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);
|
||||
}
|
||||
|
||||
@@ -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<any, Readonly<ICachedMapPageOptions>>;
|
||||
dependencies?: any[];
|
||||
}
|
||||
|
||||
interface IPagination<TResource> {
|
||||
offset: number;
|
||||
key: ResourceKeyListAlias<any, Readonly<ICachedMapPageOptions>>;
|
||||
data: CachedMapResourceListGetter<CachedMapResourceValue<TResource>, []>;
|
||||
hasNextPage: boolean;
|
||||
loaded: ResourceKeyListAlias<any, any>[];
|
||||
refresh: () => void;
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
interface IPaginationPrivate<TResource> extends IPagination<TResource> {
|
||||
pageSize: number;
|
||||
resource: CachedMapResource<any, any, any, any>;
|
||||
dependencies: any[];
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 100;
|
||||
|
||||
export function usePagination<TResource extends CachedMapResource<any, any, any, any>>(
|
||||
ctor: IServiceConstructor<TResource>,
|
||||
options?: IOptions,
|
||||
): IPagination<TResource> {
|
||||
const pageSize = options?.pageSize || DEFAULT_PAGE_SIZE;
|
||||
const resource = useService(ctor);
|
||||
|
||||
const pagination = useObservableRef<IPaginationPrivate<TResource>>(
|
||||
() => {
|
||||
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<CachedMapResourceValue<TResource>, []> {
|
||||
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;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ import { getComputed } from '../getComputed';
|
||||
import { useObjectRef } from '../useObjectRef';
|
||||
import { useObservableRef } from '../useObservableRef';
|
||||
|
||||
interface KeyWithIncludes<TKey, TIncludes> {
|
||||
export interface ResourceKeyWithIncludes<TKey, TIncludes> {
|
||||
readonly key: TKey | null;
|
||||
readonly includes: TIncludes;
|
||||
}
|
||||
@@ -66,8 +66,8 @@ interface IMapResourceState<TResource> extends ILoadableState {
|
||||
loaded: boolean;
|
||||
resource: TResource;
|
||||
isOutdated: () => boolean;
|
||||
load: () => void;
|
||||
reload: () => void;
|
||||
load: () => Promise<void>;
|
||||
reload: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface IMapResourceListResult<TResource, TIncludes> extends IMapResourceState<TResource> {
|
||||
@@ -115,7 +115,7 @@ export function useResource<
|
||||
>(
|
||||
component: { name: string },
|
||||
ctor: IServiceConstructor<TResource> | TResource,
|
||||
keyObj: TResource extends any ? TKeyArg | null | KeyWithIncludes<TKeyArg, TIncludes> : never,
|
||||
keyObj: TResource extends any ? TKeyArg | null | ResourceKeyWithIncludes<TKeyArg, TIncludes> : never,
|
||||
actions?: TResource extends any ? IActions<TResource, TKeyArg, TIncludes> : never,
|
||||
): TResult<TResource, TKeyArg, TIncludes>;
|
||||
|
||||
@@ -126,7 +126,7 @@ export function useResource<
|
||||
>(
|
||||
component: { name: string },
|
||||
ctor: IServiceConstructor<TResource> | TResource,
|
||||
keyObj: TResource extends any ? TKeyArg | null | KeyWithIncludes<TKeyArg, TIncludes> : never,
|
||||
keyObj: TResource extends any ? TKeyArg | null | ResourceKeyWithIncludes<TKeyArg, TIncludes> : never,
|
||||
actions?: TResource extends any ? IActions<TResource, TKeyArg, TIncludes> : never,
|
||||
): IMapResourceResult<TResource, TIncludes> | IMapResourceListResult<TResource, TIncludes> | IDataResourceResult<TResource, TIncludes> {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
@@ -462,6 +462,6 @@ export function useResource<
|
||||
return result;
|
||||
}
|
||||
|
||||
function isKeyWithIncludes<TKey, TIncludes>(obj: any): obj is KeyWithIncludes<TKey, TIncludes> {
|
||||
function isKeyWithIncludes<TKey, TIncludes>(obj: any): obj is ResourceKeyWithIncludes<TKey, TIncludes> {
|
||||
return obj && typeof obj === 'object' && 'includes' in obj && 'key' in obj;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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...'],
|
||||
|
||||
@@ -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...'],
|
||||
|
||||
@@ -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', 'Отмена...'],
|
||||
|
||||
@@ -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', '取消中...'],
|
||||
|
||||
@@ -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<TResource> = CachedResourceKey<TResource>;
|
||||
@@ -30,8 +30,25 @@ export type CachedMapResourceLoader<TRealKey, TKey, TValue, TIncludes> = TRealKe
|
||||
? Array<CachedResourceValueIncludes<TValue, TIncludes>>
|
||||
: CachedResourceValueIncludes<TValue, TIncludes>;
|
||||
|
||||
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<any, [offset: number, limit: number], Readonly<ICachedMapPageOptions>>(
|
||||
'@cached-map-resource/page',
|
||||
(offset: number, limit: number) => ({ offset, limit }),
|
||||
);
|
||||
|
||||
interface IPageInfo<TKey> {
|
||||
totalCount?: number;
|
||||
edges?: TKey[];
|
||||
hasNextPage?: boolean;
|
||||
}
|
||||
|
||||
type ICachedMapResourceMetadata<TKey> = ICachedResourceMetadata & IPageInfo<TKey>;
|
||||
|
||||
/**
|
||||
* CachedMapResource is a resource that stores data in a Map.
|
||||
@@ -40,7 +57,7 @@ export abstract class CachedMapResource<
|
||||
TKey,
|
||||
TValue,
|
||||
TContext extends Record<string, any> = Record<string, never>,
|
||||
TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata,
|
||||
TMetadata extends ICachedMapResourceMetadata<TKey> = ICachedMapResourceMetadata<TKey>,
|
||||
> extends CachedResource<Map<TKey, TValue>, TValue, TKey, CachedResourceIncludeArgs<TValue, TContext>, TMetadata> {
|
||||
readonly onItemUpdate: ISyncExecutor<ResourceKeySimple<TKey>>;
|
||||
readonly onItemDelete: ISyncExecutor<ResourceKeySimple<TKey>>;
|
||||
@@ -63,6 +80,11 @@ export abstract class CachedMapResource<
|
||||
this.onItemDelete = new SyncExecutor<ResourceKeySimple<TKey>>(null);
|
||||
|
||||
this.addAlias(CachedMapAllKey, () => resourceKeyList(this.keys));
|
||||
this.addAlias(CachedMapPageKey, key => {
|
||||
const pageInfo = this.getPageInfo(key as ResourceKeyFlat<TKey>);
|
||||
|
||||
return resourceKeyList(pageInfo?.edges || []);
|
||||
});
|
||||
|
||||
makeObservable<this, 'dataSet' | 'dataDelete'>(this, {
|
||||
set: action,
|
||||
@@ -121,6 +143,14 @@ export abstract class CachedMapResource<
|
||||
return ResourceKeyUtils.map(key, key => this.dataGet(this.getKeyRef(key)));
|
||||
}
|
||||
|
||||
getPageInfo(key: ResourceKeyFlat<TKey>): IPageInfo<TKey> | undefined {
|
||||
if (!this.hasMetadata(key)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.getMetadata(key);
|
||||
}
|
||||
|
||||
set(key: TKey | ResourceKeyAlias<TKey, any>, value: TValue): void;
|
||||
set(key: ResourceKeyList<TKey> | ResourceKeyListAlias<TKey, any>, value: TValue[]): void;
|
||||
set(key: ResourceKey<TKey>, value: TValue | TValue[]): void;
|
||||
@@ -225,6 +255,14 @@ export abstract class CachedMapResource<
|
||||
return super.getKeyRef(key);
|
||||
}
|
||||
|
||||
protected setPageInfo(key: ResourceKeyFlat<TKey>, pageInfo: IPageInfo<TKey>): 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
|
||||
|
||||
@@ -100,6 +100,7 @@ export abstract class CachedResource<
|
||||
|
||||
/** Need to infer value type */
|
||||
private readonly typescriptHack: TValue;
|
||||
private captureAliasGetterExecution: boolean;
|
||||
|
||||
constructor(defaultKey: ResourceKey<TKey>, 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<TKey, any> | ResourceKeyListAlias<TKey, any>,
|
||||
): ResourceKeyAlias<TKey, any> | ResourceKeyListAlias<TKey, any> {
|
||||
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<TKey>, param: ResourceAlias<TKey, any>): ResourceKey<TKey> {
|
||||
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.
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
import { isObjectsEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
export type ResourceAliasOptionsKey = string | number;
|
||||
export type ResourceAliasOptionsValue = ResourceAliasOptionsKey | Array<ResourceAliasOptionsKey>;
|
||||
export type ResourceAliasOptionsValueTypes = string | number | boolean | null | undefined;
|
||||
export type ResourceAliasOptionsValue = ResourceAliasOptionsValueTypes | Array<ResourceAliasOptionsValueTypes>;
|
||||
export type ResourceAliasOptions = Readonly<Record<ResourceAliasOptionsKey, ResourceAliasOptionsValue>> | undefined;
|
||||
|
||||
export abstract class ResourceAlias<TKey, TOptions extends ResourceAliasOptions> {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
query getAdminUserInfo($userId: ID!, $includeMetaParameters: Boolean!, $customIncludeOriginDetails: Boolean!) {
|
||||
user: adminUserInfo(userId: $userId) {
|
||||
...AdminUserInfo
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,11 @@ export interface AdminTeamInfo {
|
||||
teamPermissions: Array<Scalars['ID']>;
|
||||
}
|
||||
|
||||
export interface AdminUserFilterInput {
|
||||
enabledState?: InputMaybe<Scalars['Boolean']>;
|
||||
userIdMask?: InputMaybe<Scalars['String']>;
|
||||
}
|
||||
|
||||
export interface AdminUserInfo {
|
||||
authRole?: Maybe<Scalars['String']>;
|
||||
configurationParameters: Scalars['Object'];
|
||||
@@ -933,6 +938,11 @@ export enum ObjectPropertyLength {
|
||||
Tiny = 'TINY',
|
||||
}
|
||||
|
||||
export interface PageInput {
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
}
|
||||
|
||||
export interface ProductInfo {
|
||||
buildTime: Scalars['String'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
@@ -959,6 +969,7 @@ export interface ProjectInfo {
|
||||
|
||||
export interface Query {
|
||||
activeUser?: Maybe<UserInfo>;
|
||||
adminUserInfo: AdminUserInfo;
|
||||
authChangeLocalPassword: Scalars['Boolean'];
|
||||
authLogin: AuthInfo;
|
||||
authLogout?: Maybe<Scalars['Boolean']>;
|
||||
@@ -1043,6 +1054,10 @@ export interface Query {
|
||||
userConnections: Array<ConnectionInfo>;
|
||||
}
|
||||
|
||||
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<Scalars['ID']>;
|
||||
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<string>;
|
||||
linkedAuthProviders: Array<string>;
|
||||
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<any>;
|
||||
value?: any;
|
||||
length: ObjectPropertyLength;
|
||||
features: Array<string>;
|
||||
order: number;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetUserGrantedConnectionsQueryVariables = Exact<{
|
||||
userId: Scalars['ID'];
|
||||
}>;
|
||||
@@ -2133,7 +2185,8 @@ export type GetUserGrantedConnectionsQuery = {
|
||||
};
|
||||
|
||||
export type GetUsersListQueryVariables = Exact<{
|
||||
userId?: InputMaybe<Scalars['ID']>;
|
||||
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<GetAdminUserInfoQuery> {
|
||||
return withWrapper(
|
||||
wrappedRequestHeaders =>
|
||||
client.request<GetAdminUserInfoQuery>(GetAdminUserInfoDocument, variables, { ...requestHeaders, ...wrappedRequestHeaders }),
|
||||
'getAdminUserInfo',
|
||||
'query',
|
||||
);
|
||||
},
|
||||
getUserGrantedConnections(
|
||||
variables: GetUserGrantedConnectionsQueryVariables,
|
||||
requestHeaders?: GraphQLClientRequestHeaders,
|
||||
|
||||
+2
-10
@@ -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 {
|
||||
|
||||
+2
-3
@@ -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<ITeamFormProps> = 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<AdminUser>((user): user is AdminUser => !!user && state.state.grantedUsers.includes(user.userId)),
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+8
-3
@@ -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<void> {}
|
||||
|
||||
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) {
|
||||
|
||||
+20
-23
@@ -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<AdminUser[]>({ equals: (first, second) => isArraysEqual(first, second, undefined, true) }),
|
||||
setSearch: action.bound,
|
||||
setRole: action.bound,
|
||||
setStatus: action.bound,
|
||||
},
|
||||
{ users },
|
||||
false,
|
||||
);
|
||||
|
||||
return filters;
|
||||
|
||||
+5
-9
@@ -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<Props>(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)(
|
||||
<box>
|
||||
{controller.user ? <UserForm user={controller.user} editing onCancel={collapse} /> : <Loader />}
|
||||
</box>,
|
||||
);
|
||||
return styled(styles)(<box>{user.data ? <UserForm user={user.data as AdminUserInfo} editing onCancel={collapse} /> : <Loader />}</box>);
|
||||
});
|
||||
|
||||
-65
@@ -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<void> {
|
||||
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<string>) {
|
||||
if (!ResourceKeyUtils.isIntersect(key, this.userId)) {
|
||||
return;
|
||||
}
|
||||
this.user = JSON.parse(JSON.stringify(await this.usersResource.load(this.userId, ['includeMetaParameters'])));
|
||||
}
|
||||
}
|
||||
+14
-21
@@ -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<Props>(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)(
|
||||
<ColoredContainer vertical wrap gap parent>
|
||||
@@ -105,15 +98,15 @@ export const UsersPage = observer<Props>(function UsersPage({ sub, param }) {
|
||||
)}
|
||||
|
||||
<Group boxNoOverflow>
|
||||
<Loader style={loaderStyle} state={[usersResource, authRolesResource]} overlay>
|
||||
<UsersTable
|
||||
keys={keys}
|
||||
selectedItems={table.state.selected}
|
||||
expandedItems={table.state.expanded}
|
||||
users={filters.filteredUsers}
|
||||
displayAuthRole={displayAuthRole}
|
||||
/>
|
||||
</Loader>
|
||||
<UsersTable
|
||||
users={table.users}
|
||||
selectedItems={table.state.selected}
|
||||
expandedItems={table.state.expanded}
|
||||
displayAuthRole={displayAuthRole}
|
||||
loading={loading}
|
||||
hasMore={table.hasMore}
|
||||
onLoadMore={table.loadMore}
|
||||
/>
|
||||
</Group>
|
||||
</Container>
|
||||
</ColoredContainer>,
|
||||
|
||||
+44
-4
@@ -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<string, boolean>;
|
||||
expandedItems: Map<string, boolean>;
|
||||
displayAuthRole: boolean;
|
||||
loading?: boolean;
|
||||
hasMore: boolean;
|
||||
onLoadMore?: () => void;
|
||||
}
|
||||
|
||||
export const UsersTable = observer<Props>(function UsersTable({ keys, users, selectedItems, expandedItems, displayAuthRole }) {
|
||||
export const UsersTable = observer<Props>(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 (
|
||||
<Table keys={keys} selectedItems={selectedItems} expandedItems={expandedItems} size="big">
|
||||
@@ -31,7 +53,9 @@ export const UsersTable = observer<Props>(function UsersTable({ keys, users, sel
|
||||
<TableSelect />
|
||||
</TableColumnHeader>
|
||||
)} */}
|
||||
<TableColumnHeader min />
|
||||
<TableColumnHeader min>
|
||||
<Loader loading={loading} small />
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader>{translate('authentication_user_name')}</TableColumnHeader>
|
||||
{displayAuthRole && <TableColumnHeader>{translate('authentication_user_role')}</TableColumnHeader>}
|
||||
<TableColumnHeader>{translate('authentication_user_team')}</TableColumnHeader>
|
||||
@@ -47,6 +71,22 @@ export const UsersTable = observer<Props>(function UsersTable({ keys, users, sel
|
||||
// selectable={isLocalProviderAvailable}
|
||||
/>
|
||||
))}
|
||||
{(loading || users.length === 0) && (
|
||||
<TableItem item="load-more">
|
||||
<TableColumnValue colSpan={colSpan} centerContent flex>
|
||||
{translate(loading ? 'ui_processing_loading' : 'authentication_administration_users_empty')}
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
)}
|
||||
{hasMore && (
|
||||
<TableItem item="load-more">
|
||||
<TableColumnValue colSpan={colSpan} centerContent flex>
|
||||
<Button type="button" mod={['outlined']} loading={loading} loader onClick={onLoadMore}>
|
||||
{translate('ui_load_more')}
|
||||
</Button>
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
+34
-16
@@ -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<void>;
|
||||
delete: () => Promise<void>;
|
||||
}
|
||||
|
||||
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<AdminUser[]>({ equals: (first, second) => isArraysEqual(first, second, undefined, true) }),
|
||||
update: action.bound,
|
||||
delete: action.bound,
|
||||
loadMore: action.bound,
|
||||
},
|
||||
{ usersResource },
|
||||
{ usersLoader, loadableState: usersLoader },
|
||||
);
|
||||
|
||||
return state;
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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', 'Учетные данные пользователя удалены'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
+2
-2
@@ -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<IConnectionFormProps>
|
||||
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user