diff --git a/webapp/packages/core-blocks/src/Table/TableItem.tsx b/webapp/packages/core-blocks/src/Table/TableItem.tsx index 3cf917496e..f3817011b4 100644 --- a/webapp/packages/core-blocks/src/Table/TableItem.tsx +++ b/webapp/packages/core-blocks/src/Table/TableItem.tsx @@ -78,10 +78,10 @@ export const TableItem = observer(function TableItem({ > {children} - {isExpanded && ( + {isExpanded && ExpandElement && ( - {ExpandElement && } + )} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx index f0aef06ad7..ecc04c9736 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx @@ -146,19 +146,19 @@ export const ConnectionEdit = observer(function ConnectionEdit({ {controller.isLoading diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts index 3d281aca86..15aa831ad3 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEditController.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { observable, action } from 'mobx'; +import { observable, action, computed } from 'mobx'; import { injectable, IInitializableController, IDestructibleController, Bootstrap @@ -47,6 +47,10 @@ implements IInitializableController, IDestructibleController { credentials: {}, }; + @computed get isDisabled() { + return this.isLoading || this.isSaving; + } + get isNew() { return this.connectionsResource.isNew(this.connectionId); } @@ -180,13 +184,12 @@ implements IInitializableController, IDestructibleController { await this.connectionsResource.load(this.connectionId); this.connectionInfo = this.connectionsResource.get(this.connectionId)!; + await this.loadDriver(this.connectionInfo.driverId); } catch (exception) { this.notificationService.logException(exception, `Can't load ConnectionInfo ${this.connectionId}`); } finally { this.isLoading = false; } - - await this.loadDriver(this.connectionInfo.driverId); } private async loadDriver(driverId: string) { diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx index c6ca725a78..59b4147361 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx @@ -42,7 +42,7 @@ export const ConnectionForm = observer(function ConnectionForm({ checkboxLabel={translate('connections_connection_template')} checked={controller.config.template} onChange={value => controller.onChange('template', value)} - disabled={!controller.isNew || controller.isSaving} + disabled={!controller.isNew || controller.isDisabled} mod='surface' /> @@ -65,7 +65,7 @@ export const ConnectionForm = observer(function ConnectionForm({ name="name" value={controller.config.name} onChange={value => controller.onChange('name', value)} - disabled={controller.isSaving} + disabled={controller.isDisabled} mod='surface' > {translate('connections_connection_name')} @@ -77,7 +77,7 @@ export const ConnectionForm = observer(function ConnectionForm({ rows={3} value={controller.config.description} onChange={value => controller.onChange('description', value)} - disabled={controller.isSaving} + disabled={controller.isDisabled} mod='surface' > {translate('connections_connection_description')} @@ -92,7 +92,7 @@ export const ConnectionForm = observer(function ConnectionForm({ value={'custom'} onClick={() => controller.onChangeType(ConnectionType.Attributes)} checked={controller.connectionType === ConnectionType.Attributes} - disabled={controller.isSaving} + disabled={controller.isDisabled} mod={['primary']} > {translate('customConnection_connectionType_custom')} @@ -103,7 +103,7 @@ export const ConnectionForm = observer(function ConnectionForm({ value={'url'} onClick={() => controller.onChangeType(ConnectionType.URL)} checked={controller.connectionType === ConnectionType.URL} - disabled={controller.isSaving} + disabled={controller.isDisabled} mod={['primary']} > {translate('customConnection_connectionType_url')} @@ -118,7 +118,7 @@ export const ConnectionForm = observer(function ConnectionForm({ name="url" value={controller.config.url} onChange={value => controller.onChange('url', value)} - disabled={controller.isSaving} + disabled={controller.isDisabled} mod='surface' > {translate('customConnection_url_JDBC')} @@ -135,7 +135,7 @@ export const ConnectionForm = observer(function ConnectionForm({ autofillToken={`section-${controller.driver?.id || ''} section-auth`} properties={controller.authModel.properties} credentials={controller.config.credentials} - processing={controller.isSaving} + processing={controller.isDisabled} /> )} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts index 9609d90375..572e89fc62 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/IFormController.ts @@ -20,6 +20,7 @@ export interface IFormController { config: ConnectionConfig; connectionType: ConnectionType; isSaving: boolean; + isDisabled: boolean; onChangeType(type: ConnectionType): void; onSelectDriver(driver: DBDriver): void; onChange(property: T, value: ConnectionConfig[T]): void; diff --git a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts index 8853e141bb..8fced80cdc 100644 --- a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts +++ b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts @@ -15,25 +15,40 @@ import { ResourceKey, isResourceKeyList, } from '@cloudbeaver/core-sdk'; -import { uuid } from '@cloudbeaver/core-utils'; +import { uuid, MetadataMap } from '@cloudbeaver/core-utils'; + +const NEW_CONNECTION_SYMBOL = Symbol('new-connection'); +type ConnectionNew = ConnectionInfo & { [NEW_CONNECTION_SYMBOL]: boolean } @injectable() export class ConnectionsResource extends CachedMapResource { - constructor( - private graphQLService: GraphQLService, - ) { + private metadata: MetadataMap; + constructor(private graphQLService: GraphQLService) { super(new Map()); + this.metadata = new MetadataMap(() => false); + } + + has(id: string) { + if (this.metadata.has(id)) { + return this.metadata.get(id); + } + + return this.data.has(id); } isNew(id: string) { - return id.startsWith('new-'); + if (!this.has(id)) { + return false; + } + return NEW_CONNECTION_SYMBOL in this.get(id)!; } addNew() { const connectionInfo = { id: `new-${uuid()}`, name: 'New connection', - } as ConnectionInfo; + [NEW_CONNECTION_SYMBOL]: true, + } as ConnectionNew; this.data.set(connectionInfo.id, connectionInfo); this.markUpdated(connectionInfo.id); @@ -52,7 +67,7 @@ export class ConnectionsResource extends CachedMapResource extends CachedResource< isLoaded(key: ResourceKey): boolean { if (isResourceKeyList(key)) { - return key.list.every(key => this.data.has(key)); + return key.list.every(key => this.has(key)); } - return this.data.has(key); + return this.has(key); } isDataLoading(key: ResourceKey): boolean { @@ -142,6 +142,10 @@ export abstract class CachedMapResource extends CachedResource< await this.loadData(key); return this.get(key) as Array| TValue; } + + has(key: TKey): boolean { + return this.data.has(key); + } } export function isResourceKeyList(data: any): data is ResourceKeyList { diff --git a/webapp/packages/core-sdk/src/queries/authentication/users/revokeUserRole.gql b/webapp/packages/core-sdk/src/queries/authentication/users/revokeUserRole.gql new file mode 100644 index 0000000000..2590b783a9 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/users/revokeUserRole.gql @@ -0,0 +1,3 @@ +query revokeUserRole($userId: ID!, $roleId: ID!) { + revokeUserRole(userId: $userId, roleId: $roleId) +} \ 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 7353c1d054..836a9de8d4 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -846,6 +846,13 @@ export type GrantUserRoleQueryVariables = Exact<{ export type GrantUserRoleQuery = Pick; +export type RevokeUserRoleQueryVariables = Exact<{ + userId: Scalars['ID']; + roleId: Scalars['ID']; +}>; + +export type RevokeUserRoleQuery = Pick; + export type SetUserCredentialsQueryVariables = Exact<{ userId: Scalars['ID']; providerId: Scalars['ID']; @@ -1330,6 +1337,11 @@ export const GrantUserRoleDocument = ` grantUserRole(userId: $userId, roleId: $roleId) } `; +export const RevokeUserRoleDocument = ` + query revokeUserRole($userId: ID!, $roleId: ID!) { + revokeUserRole(userId: $userId, roleId: $roleId) +} + `; export const SetUserCredentialsDocument = ` query setUserCredentials($userId: ID!, $providerId: ID!, $credentials: Object!) { setUserCredentials(userId: $userId, providerId: $providerId, credentials: $credentials) @@ -2067,6 +2079,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = grantUserRole(variables: GrantUserRoleQueryVariables): Promise { return withWrapper(() => client.request(GrantUserRoleDocument, variables)); }, + revokeUserRole(variables: RevokeUserRoleQueryVariables): Promise { + return withWrapper(() => client.request(RevokeUserRoleDocument, variables)); + }, setUserCredentials(variables: SetUserCredentialsQueryVariables): Promise { return withWrapper(() => client.request(SetUserCredentialsDocument, variables)); }, diff --git a/webapp/packages/core-utils/src/MetadataMap.ts b/webapp/packages/core-utils/src/MetadataMap.ts index e952895721..78d56b8fb2 100644 --- a/webapp/packages/core-utils/src/MetadataMap.ts +++ b/webapp/packages/core-utils/src/MetadataMap.ts @@ -39,6 +39,14 @@ export class MetadataMap { return this.length; } + has(key: TKey): boolean { + return this.data.has(key); + } + + set(key: TKey, value: TValue) { + this.data.set(key, value); + } + get(key: TKey): TValue { if (this.data.has(key)) { return this.data.get(key)!; diff --git a/webapp/packages/plugin-authentication/src/Administration/RolesResource.ts b/webapp/packages/plugin-authentication/src/Administration/RolesResource.ts index 3aa2f65c7f..e316d31f24 100644 --- a/webapp/packages/plugin-authentication/src/Administration/RolesResource.ts +++ b/webapp/packages/plugin-authentication/src/Administration/RolesResource.ts @@ -9,40 +9,52 @@ import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService, - CachedDataResource, - AdminRoleInfo + AdminRoleInfo, + CachedMapResource, + ResourceKey } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; @injectable() -export class RolesResource extends CachedDataResource { - constructor( - private graphQLService: GraphQLService, - ) { - super([]); +export class RolesResource extends CachedMapResource { + private metadata: MetadataMap; + constructor(private graphQLService: GraphQLService) { + super(new Map()); + this.metadata = new MetadataMap(() => false); } - isLoaded(roleId?: string) { - return roleId - ? this.data.some(role => role.roleId === roleId) - : !!this.data.length; + has(id: string) { + if (this.metadata.has(id)) { + return this.metadata.get(id); + } + + return this.data.has(id); } - protected async loader(roleId?: string): Promise { + async loadAll() { + await this.load('all'); + return this.data; + } + + async refreshAll() { + await this.refresh('all'); + return this.data; + } + + protected async loader(key: ResourceKey): Promise> { + const roleId = key === 'all' ? undefined : key as string; + const { roles } = await this.graphQLService.gql.getRolesList({ roleId }); - if (!roleId) { - this.markUpdated(roleId); - return roles as AdminRoleInfo[]; + if (key === 'all') { + this.data.clear(); + this.metadata.set('all', true); } - const index = this.data.findIndex(role => role.roleId === roleId); - if (index !== -1) { - this.data.splice(index, 1, ...roles as AdminRoleInfo[]); - } else { - this.data.push(...roles as AdminRoleInfo[]); + for (const role of roles) { + this.set(role.roleId, role as AdminRoleInfo); } - - this.markUpdated(roleId); + this.markUpdated(key); return this.data; } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser/CreateUser.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser/CreateUser.tsx deleted file mode 100644 index c367f16353..0000000000 --- a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser/CreateUser.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 { observer } from 'mobx-react'; -import { useCallback } from 'react'; -import styled, { css } from 'reshadow'; - -import { - InputField, SubmittingForm, ErrorMessage, Button -} from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; -import { useTranslate } from '@cloudbeaver/core-localization'; -import { useStyles, composes } from '@cloudbeaver/core-theming'; - -import { CreateUserController } from './CreateUserController'; - -const styles = composes( - css` - `, - css` - SubmittingForm { - display: flex; - flex: 1; - } - create-form { - flex: 1; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: center; - } - group { - box-sizing: border-box; - display: flex; - margin: 0 12px; - } - controls { - display: flex; - align-items: center; - justify-content: flex-end; - } - item-title { - composes: theme-typography--headline5 from global; - padding: 16px; - } - ` -); - -export const CreateUser = observer(function CreateUser() { - const controller = useController(CreateUserController); - const translate = useTranslate(); - const handleChange = useCallback( - (value: string) => controller.login = value, - [] - ); - - return styled(useStyles(styles))( - <> - - User creation - - - - - - {translate('User name')} - - - - - - - - - {controller.error.responseMessage && ( - - )} - - ); -}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser/CreateUserController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser/CreateUserController.ts deleted file mode 100644 index b177936b21..0000000000 --- a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser/CreateUserController.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 { observable } from 'mobx'; - -import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { GQLErrorCatcher } from '@cloudbeaver/core-sdk'; - -import { UsersManagerService } from '../../UsersManagerService'; - -@injectable() -export class CreateUserController { - @observable isCreating = false; - @observable login = ''; - - readonly error = new GQLErrorCatcher(); - - constructor( - private notificationService: NotificationService, - private administrationScreenService: AdministrationScreenService, - private usersManagerService: UsersManagerService, - private commonDialogService: CommonDialogService, - ) { } - - create = async () => { - if (this.isCreating) { - return; - } - - this.isCreating = true; - try { - await this.usersManagerService.create(this.login); - this.administrationScreenService.navigateToItem('users'); - } catch (exception) { - if (!this.error.catch(exception)) { - this.notificationService.logException(exception, 'User create failed'); - } - } finally { - this.isCreating = false; - } - } - - showDetails = () => { - if (this.error.exception) { - this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); - } - } -} diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx deleted file mode 100644 index f54e4e15c0..0000000000 --- a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialog.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 { observer } from 'mobx-react'; -import { useCallback } from 'react'; -import styled, { css } from 'reshadow'; - -import { - SubmittingForm, ErrorMessage, InputField, Checkbox, useFocus -} from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; -import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; -import { useTranslate } from '@cloudbeaver/core-localization'; -import { composes, useStyles } from '@cloudbeaver/core-theming'; - -import { CreateUserDialogController } from './CreateUserDialogController'; -import { CreateUserDialogFooter } from './CreateUserDialogFooter'; - -const styles = composes( - css` - Tab { - composes: theme-ripple theme-background-secondary theme-text-on-secondary from global; - } - ErrorMessage { - composes: theme-background-secondary from global; - } - `, - css` - CommonDialogWrapper { - min-height: 400px; - min-width: 600px; - } - SubmittingForm { - overflow: auto; - margin-top: 20px; - margin-bottom: 20px; - } - SubmittingForm, create-form { - flex: 1; - display: flex; - flex-direction: column; - } - create-form { - flex-direction: column; - padding: 18px 24px; - } - ErrorMessage { - position: sticky; - bottom: 0; - padding: 8px 24px; - } - ` -); - -export const CreateUserDialog = observer(function CreateUserDialog({ - rejectDialog, -}: DialogComponentProps) { - const [focusedRef] = useFocus({ focusFirstChild: true }); - const controller = useController(CreateUserDialogController, rejectDialog); - const translate = useTranslate(); - const handleLoginChange = useCallback( - (value: string) => controller.credentials.login = value, - [] - ); - const handlePasswordChange = useCallback( - (value: string) => controller.credentials.password = value, - [] - ); - const handlePasswordRepeatChange = useCallback( - (value: string) => controller.credentials.passwordRepeat = value, - [] - ); - const handleRoleChange = useCallback( - (roleId: string, value: boolean) => controller.credentials.roles.set(roleId, value), - [] - ); - - return styled(useStyles(styles))( - - )} - onReject={rejectDialog} - > - }> - - - - {translate('authentication_user_name')} - - - - - {translate('authentication_user_password')} - - - - - {translate('authentication_user_password_repeat')} - - - {controller.roles.map((role, i) => ( - - handleRoleChange(role.roleId, checked)} - checked={controller.credentials.roles.get(role.roleId)} - disabled={controller.isCreating} - mod='surface' - > - {i === 0 && translate('authentication_user_role')} - - - ))} - - - {controller.error.responseMessage && ( - - )} - - ); -}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogFooter.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogFooter.tsx deleted file mode 100644 index 8af436ef00..0000000000 --- a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogFooter.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 { observer } from 'mobx-react'; -import styled, { css } from 'reshadow'; - -import { Button } from '@cloudbeaver/core-blocks'; -import { useTranslate } from '@cloudbeaver/core-localization'; - -const styles = css` - controls { - display: flex; - height: 100%; - flex: 1; - align-items: center; - margin: auto; - justify-content: flex-end; - } - - fill { - flex: 1; - } -`; - -export type Props = { - isFormFilled: boolean; - isCreating: boolean; - onCancel(): void; - onCreate(): void; -} - -export const CreateUserDialogFooter = observer( - function CreateUserDialogFooter({ - isFormFilled, - isCreating, - onCancel, - onCreate, - }: Props) { - const translate = useTranslate(); - - return styled(styles)( - - - - - - ); - } -); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministration.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministration.tsx index de4ee713e2..93c2d7ef72 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministration.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministration.tsx @@ -57,7 +57,11 @@ export const UsersAdministration = observer(function UsersAdministration() { - + {controller.isLoading && } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts index 672d0e7be7..61bcb07763 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts @@ -14,34 +14,45 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; import { GQLErrorCatcher } from '@cloudbeaver/core-sdk'; -import { UsersManagerService } from '../UsersManagerService'; -import { CreateUserDialog } from './CreateUserDialog/CreateUserDialog'; +import { UsersResource } from '../UsersResource'; @injectable() export class UsersAdministrationController { @observable isDeleting = false; readonly selectedItems = observable(new Map()) + readonly expandedItems = observable(new Map()) readonly error = new GQLErrorCatcher(); get users() { - return this.usersManagerService.users.data; + return Array.from(this.usersResource.data.values()) + .sort((a, b) => { + if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) { + return 0; + } + if (this.usersResource.isNew(a.userId)) { + return -1; + } + return 1; + }); } + get isLoading() { - return this.usersManagerService.users.isLoading(); + return this.usersResource.isLoading(); } constructor( private notificationService: NotificationService, - private usersManagerService: UsersManagerService, + private usersResource: UsersResource, private commonDialogService: CommonDialogService, ) { } create = () => { - this.commonDialogService.open(CreateUserDialog, null); + const user = this.usersResource.addNew(); + this.expandedItems.set(user.userId, true); } update = async () => { try { - await this.usersManagerService.users.refresh(undefined); + await this.usersResource.refreshAll(); } catch (exception) { if (!this.error.catch(exception)) { this.notificationService.logException(exception, 'Users update failed'); @@ -75,10 +86,10 @@ export class UsersAdministrationController { } for (const userId of deletionList) { - await this.usersManagerService.delete(userId); + await this.usersResource.delete(userId); } this.selectedItems.clear(); - await this.usersManagerService.users.refresh(undefined); + await this.usersResource.loadAll(); } catch (exception) { if (!this.error.catch(exception)) { this.notificationService.logException(exception, 'User delete failed'); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts index 715a2a46a0..e4ecf41274 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts @@ -10,8 +10,7 @@ import { AdministrationItemService, AdministrationScreenService } from '@cloudbe import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { UsersManagerService } from '../UsersManagerService'; -import { CreateUser } from './CreateUser/CreateUser'; +import { UsersResource } from '../UsersResource'; import { UsersAdministration } from './UsersAdministration'; import { UsersDrawerItem } from './UsersDrawerItem'; @@ -21,7 +20,7 @@ export class UsersAdministrationService extends Bootstrap { private administrationItemService: AdministrationItemService, private administrationScreenService: AdministrationScreenService, private notificationService: NotificationService, - private usersManagerService: UsersManagerService, + private usersResource: UsersResource, ) { super(); } @@ -31,10 +30,6 @@ export class UsersAdministrationService extends Bootstrap { name: 'users', order: 1, sub: [ - { - name: 'create', - getComponent: () => CreateUser, - }, { name: 'edit', getComponent: () => UsersAdministration, @@ -52,17 +47,13 @@ export class UsersAdministrationService extends Bootstrap { this.administrationScreenService.navigateToItem('users'); } - navToCreate() { - this.administrationScreenService.navigateToItemSub('users', 'create'); - } - navToEdit(userId: string) { this.administrationScreenService.navigateToItemSub('users', 'edit', userId); } private async loadUsers() { try { - await this.usersManagerService.users.load(undefined); + await this.usersResource.loadAll(); } catch (exception) { this.notificationService.logException(exception, 'Error occurred while loading users'); } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx index 902bf35e3b..9fe4370537 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx @@ -7,22 +7,45 @@ */ import { observer } from 'mobx-react'; -import styled from 'reshadow'; +import styled, { css, use } from 'reshadow'; import { - TableItem, TableColumnValue, TableItemSelect + TableItem, TableColumnValue, TableItemSelect, TableItemExpand } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; import { AdminUserInfo } from '@cloudbeaver/core-sdk'; -import { useStyles } from '@cloudbeaver/core-theming'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { UsersResource } from '../../UsersResource'; +import { UserEdit } from './UserEdit'; + +const styles = composes( + css` + TableColumnValue { + composes: theme-border-color-color-positive from global; + } + `, + css` + [|new] { + border-left: solid 3px; + } + ` +); type Props = { user: AdminUserInfo; } export const User = observer(function User({ user }: Props) { - return styled(useStyles())( - - + const usersResource = useService(UsersResource); + const isNew = usersResource.isNew(user.userId); + + return styled(useStyles(styles))( + + + + + {user.userId} {user.grantedRoles.join(', ')} diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEdit.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEdit.tsx new file mode 100644 index 0000000000..790e83e224 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEdit.tsx @@ -0,0 +1,266 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { observer } from 'mobx-react'; +import { useContext, useCallback } from 'react'; +import styled, { css, use } from 'reshadow'; + +import { + TabsState, TabList, Tab, + TabTitle, Loader, SubmittingForm, TabPanel, + ErrorMessage, Button, TableItemContext, + TableContext, + InputField, + Checkbox, + useFocus, + InputGroup +} from '@cloudbeaver/core-blocks'; +import { useService, useController } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { UsersResource } from '../../UsersResource'; +import { UserEditController } from './UserEditController'; + +const styles = composes( + css` + Tab { + composes: theme-ripple theme-background-secondary theme-text-on-secondary from global; + } + + TabList { + composes: theme-border-color-background from global; + } + + ErrorMessage { + composes: theme-background-secondary from global; + } + + SubmittingForm { + composes: theme-background-secondary from global; + } + `, + css` + custom-connection { + display: flex; + flex-direction: column; + box-sizing: border-box; + } + + CommonDialogWrapper { + max-height: 500px; + min-height: 500px; + } + + SubmittingForm, BaseTabPanel { + flex: 1; + display: flex; + flex-direction: column; + } + + TabList { + align-items: center; + box-sizing: border-box; + display: inline-flex; + width: 100%; + padding-left: 24px; + outline: none; + border-bottom: solid 1px; + } + + Tab { + composes: theme-typography--body2 from global; + text-transform: uppercase; + font-weight: normal; + + &:global([aria-selected=true]) { + font-weight: normal !important; + } + } + + ErrorMessage { + position: sticky; + bottom: 0; + padding: 8px 24px; + } + + fill { + flex: 1; + } + + connection-form[|new] { + border-left: solid 3px; + } + + SubmittingForm, Loader { + min-height: 320px; + max-height: 500px; + } + + IconButton { + height: 32px; + width: 32px; + margin-right: 16px; + } + + Button:not(:first-child) { + margin-right: 24px; + } + + layout-grid { + flex: 1; + width: 100%; + } + ` +); + +type Props = { + item: string; +} + +export const UserEdit = observer(function UserEdit({ + item, +}: Props) { + const tableContext = useContext(TableContext); + const context = useContext(TableItemContext); + + const translate = useTranslate(); + const controller = useController(UserEditController, item); + const usersResource = useService(UsersResource); + const [focusedRef] = useFocus({ focusFirstChild: true }); + + const handleCancel = useCallback(() => { + tableContext?.setItemExpand(context?.item, false); + if (controller.isNew) { + usersResource.delete(item); + } + }, [tableContext, context]); + + const handleLoginChange = useCallback( + (value: string) => controller.credentials.login = value, + [] + ); + const handlePasswordChange = useCallback( + (value: string) => controller.credentials.password = value, + [] + ); + const handlePasswordRepeatChange = useCallback( + (value: string) => controller.credentials.passwordRepeat = value, + [] + ); + const handleRoleChange = useCallback( + (roleId: string, value: boolean) => controller.credentials.roles.set(roleId, value), + [] + ); + + return styled(useStyles(styles))( + + + + {translate('authentication_administration_user_info')} + + + {translate('authentication_administration_user_connections_access')} + + + + + + {controller.isLoading + ? + : ( + }> + + + + + {translate('authentication_user_credentials')} + + + + {translate('authentication_user_name')} + + + + + {translate('authentication_user_password')} + + + + + {translate('authentication_user_password_repeat')} + + + + + + {translate('authentication_user_role')} + + {controller.roles.map((role, i) => ( + + handleRoleChange(role.roleId, checked)} + checked={controller.credentials.roles.get(role.roleId)} + disabled={controller.isCreating} + mod='surface' + /> + + ))} + + + + + )} + {controller.error.responseMessage && ( + + )} + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEditController.ts similarity index 60% rename from webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogController.ts rename to webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEditController.ts index 42d4fbc749..7b7cf96452 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUserDialog/CreateUserDialogController.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEditController.ts @@ -12,14 +12,15 @@ import { injectable, IInitializableController, IDestructibleController } from '@ import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { GQLErrorCatcher } from '@cloudbeaver/core-sdk'; +import { GQLErrorCatcher, AdminUserInfo } from '@cloudbeaver/core-sdk'; import { RolesManagerService } from '../../RolesManagerService'; -import { UsersManagerService } from '../../UsersManagerService'; +import { UsersResource } from '../../UsersResource'; @injectable() -export class CreateUserDialogController implements IInitializableController, IDestructibleController { +export class UserEditController implements IInitializableController, IDestructibleController { @observable isCreating = false; + @observable isLoading = true; @observable credentials = { login: '', password: '', @@ -27,6 +28,10 @@ export class CreateUserDialogController implements IInitializableController, IDe roles: new Map(), }; + get isNew() { + return this.usersResource.isNew(this.userId); + } + @computed get isFormFilled() { const rolesState = Array.from(this.credentials.roles.values()) .filter(Boolean); @@ -38,21 +43,26 @@ export class CreateUserDialogController implements IInitializableController, IDe } @computed get roles() { - return this.rolesManagerService.roles.data; + return Array.from(this.rolesManagerService.roles.data.values()); } + + @computed get user(): AdminUserInfo { + return this.usersResource.get(this.userId)!; + } + readonly error = new GQLErrorCatcher(); private isDistructed = false; - private close!: () => void; + private userId!: string; constructor( private notificationService: NotificationService, - private usersManagerService: UsersManagerService, private commonDialogService: CommonDialogService, private rolesManagerService: RolesManagerService, + private usersResource: UsersResource, ) { } - init(onClose: () => void) { - this.close = onClose; + init(id: string) { + this.userId = id; this.loadRoles(); } @@ -60,10 +70,16 @@ export class CreateUserDialogController implements IInitializableController, IDe this.isDistructed = true; } - create = async () => { + save = async () => { if (this.isCreating) { return; } + + if (!this.credentials.password && this.isNew) { + this.notificationService.logError({ title: 'authentication_user_password_not_set' }); + return; + } + if (this.credentials.password !== this.credentials.passwordRepeat) { this.notificationService.logError({ title: 'authentication_user_passwords_not_match' }); return; @@ -72,17 +88,25 @@ export class CreateUserDialogController implements IInitializableController, IDe this.isCreating = true; let isUserCreated = false; try { - const user = await this.usersManagerService.create(this.credentials.login, false); - isUserCreated = !!user; - await this.usersManagerService.updateCredentials(user.userId, { password: this.credentials.password }); + if (this.isNew) { + await this.usersResource.create(this.credentials.login, this.userId); + } + isUserCreated = !!this.user; + + if (this.credentials.password) { + await this.usersResource.updateCredentials(this.user.userId, { password: this.credentials.password }); + } for (const [roleId, checked] of this.credentials.roles) { if (checked) { - await this.usersManagerService.grantRole(user.userId, roleId); + if (!this.user.grantedRoles.includes(roleId)) { + await this.usersResource.grantRole(this.user.userId, roleId); + } + } else if (!this.isNew) { + await this.usersResource.revokeRole(this.user.userId, roleId); } } - await this.usersManagerService.users.refresh(user.userId); + await this.usersResource.refresh(this.user.userId); this.notificationService.logInfo({ title: 'authentication_user_user_created' }); - this.close(); } catch (exception) { if (isUserCreated) { await this.deleteUser(this.credentials.login); @@ -103,7 +127,7 @@ export class CreateUserDialogController implements IInitializableController, IDe private async deleteUser(userId: string) { try { - await this.usersManagerService.delete(userId); + await this.usersResource.delete(userId); } catch (exception) { if (!this.error.catch(exception) || this.isDistructed) { this.notificationService.logException(exception, 'Error deleting partially created user'); @@ -113,9 +137,23 @@ export class CreateUserDialogController implements IInitializableController, IDe private async loadRoles() { try { - await this.rolesManagerService.roles.load(undefined); + await this.rolesManagerService.roles.loadAll(); + await this.loadUser(); } catch (exception) { this.notificationService.logException(exception, 'Can\'t load roles'); + } finally { + this.isLoading = false; + } + } + + private async loadUser() { + try { + await this.usersResource.load(this.userId); + + this.credentials.login = this.user.userId; + this.credentials.roles = new Map(this.user.grantedRoles.map(roleId => ([roleId, true]))); + } catch (exception) { + this.notificationService.logException(exception, 'Can\'t load user'); } } } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UsersTable.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UsersTable.tsx index dc1a0c2580..13d74bc8eb 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UsersTable.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UsersTable.tsx @@ -14,28 +14,26 @@ import { } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { AdminUserInfo } from '@cloudbeaver/core-sdk'; -import { useStyles, composes } from '@cloudbeaver/core-theming'; +import { useStyles } from '@cloudbeaver/core-theming'; import { User } from './User'; -const styles = composes( - css``, - css` - TableColumnHeader { - border-top: solid 1px; - } - ` -); +const styles = css` + TableColumnHeader { + border-top: solid 1px; + } +`; type Props = { users: AdminUserInfo[]; selectedItems: Map; + expandedItems: Map; } -export const UsersTable = observer(function UsersTable({ users, selectedItems }: Props) { +export const UsersTable = observer(function UsersTable({ users, selectedItems, expandedItems }: Props) { const translate = useTranslate(); return styled(useStyles(styles))( - +
{translate('authentication_user_name')} diff --git a/webapp/packages/plugin-authentication/src/Administration/UsersManagerService.ts b/webapp/packages/plugin-authentication/src/Administration/UsersManagerService.ts deleted file mode 100644 index bdc85a0e95..0000000000 --- a/webapp/packages/plugin-authentication/src/Administration/UsersManagerService.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 { injectable } from '@cloudbeaver/core-di'; -import { GraphQLService, AdminUserInfo } from '@cloudbeaver/core-sdk'; - -import { AuthInfoService } from '../AuthInfoService'; -import { AuthProviderService } from '../AuthProviderService'; -import { UsersResource } from './UsersResource'; - -@injectable() -export class UsersManagerService { - constructor( - private graphQLService: GraphQLService, - private authProviderService: AuthProviderService, - private authInfoService: AuthInfoService, - readonly users: UsersResource - ) { - } - - async create(userId: string, update?: boolean): Promise { - const { user } = await this.graphQLService.gql.createUser({ userId }); - - if (update) { - // TODO: maybe better to do refresh - this.users.data.push(user as AdminUserInfo); - } - return user as AdminUserInfo; - } - - async grantRole(userId: string, roleId: string, update?: boolean) { - await this.graphQLService.gql.grantUserRole({ userId, roleId }); - if (update) { - await this.users.refresh(userId); - } - } - - async delete(userId: string, update?: boolean) { - if (this.authInfoService.userInfo?.userId === userId) { - throw new Error('You can\'t delete current logged user'); - } - await this.graphQLService.gql.deleteUser({ userId }); - - if (update) { - await this.users.refresh(userId); - } - } - - async updateCredentials(userId: string, credentials: Record) { - const provider = 'local'; - const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); - - await this.graphQLService.gql.setUserCredentials({ - providerId: provider, - userId, - credentials: processedCredentials, - }); - } -} diff --git a/webapp/packages/plugin-authentication/src/Administration/UsersResource.ts b/webapp/packages/plugin-authentication/src/Administration/UsersResource.ts index d3db4be5f4..4ec71c4b1b 100644 --- a/webapp/packages/plugin-authentication/src/Administration/UsersResource.ts +++ b/webapp/packages/plugin-authentication/src/Administration/UsersResource.ts @@ -9,41 +9,145 @@ import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService, - CachedDataResource, - AdminUserInfo + CachedMapResource, + AdminUserInfo, + ResourceKey, + isResourceKeyList } from '@cloudbeaver/core-sdk'; +import { MetadataMap, uuid } from '@cloudbeaver/core-utils'; + +import { AuthInfoService } from '../AuthInfoService'; +import { AuthProviderService } from '../AuthProviderService'; + +const NEW_USER_SYMBOL = Symbol('new-user'); @injectable() -export class UsersResource extends CachedDataResource { +export class UsersResource extends CachedMapResource { + private metadata: MetadataMap; constructor( private graphQLService: GraphQLService, + private authProviderService: AuthProviderService, + private authInfoService: AuthInfoService, ) { - super([]); + super(new Map()); + this.metadata = new MetadataMap(() => false); } - isLoaded(userId?: string) { - return userId - ? this.data.some(role => role.userId === userId) - : !!this.data.length; + isNew(id: string): boolean { + if (!this.has(id)) { + return false; + } + return NEW_USER_SYMBOL in this.get(id)!; } - protected async loader(userId?: string): Promise { + has(id: string) { + if (this.metadata.has(id)) { + return this.metadata.get(id); + } + + return this.data.has(id); + } + + addNew() { + const user = { + userId: `new-${uuid()}`, + grantedRoles: [], + configurationParameters: {}, + metaParameters: {}, + [NEW_USER_SYMBOL]: true, + } as AdminUserInfo; + + this.data.set(user.userId, user); + this.markUpdated(user.userId); + + return user; + } + + async create(userId: string, id?: string): Promise { + const { user } = await this.graphQLService.gql.createUser({ userId }); + + if (id) { + this.data.delete(id); + } + this.set(user.userId, user as AdminUserInfo); + + return this.get(user.userId)!; + } + + async grantRole(userId: string, roleId: string) { + await this.graphQLService.gql.grantUserRole({ userId, roleId }); + await this.refresh(userId); + } + + async revokeRole(userId: string, roleId: string) { + await this.graphQLService.gql.revokeUserRole({ userId, roleId }); + await this.refresh(userId); + } + + async updateCredentials(userId: string, credentials: Record) { + const provider = 'local'; + const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); + + await this.graphQLService.gql.setUserCredentials({ + providerId: provider, + userId, + credentials: processedCredentials, + }); + } + + async delete(key: ResourceKey) { + if (isResourceKeyList(key)) { + for (let i = 0; i < key.list.length; i++) { + if (this.isActiveUser(key.list[i])) { + throw new Error('You can\'t delete current logged user'); + } + this.data.delete(key.list[i]); + if (!this.isNew(key.list[i])) { + await this.graphQLService.gql.deleteUser({ userId: key.list[i] }); + } + } + } else { + if (this.isActiveUser(key)) { + throw new Error('You can\'t delete current logged user'); + } + this.data.delete(key); + if (!this.isNew(key)) { + await this.graphQLService.gql.deleteUser({ userId: key }); + } + } + this.markUpdated(key); + this.itemDeleteSubject.next(key); + } + + async loadAll() { + await this.load('all'); + return this.data; + } + + async refreshAll() { + await this.refresh('all'); + return this.data; + } + + protected async loader(key: ResourceKey): Promise> { + const userId = key === 'all' ? undefined : key as string; + const { users } = await this.graphQLService.gql.getUsersList({ userId }); - if (!userId) { - this.markUpdated(userId); - return users as AdminUserInfo[]; + if (key === 'all') { + this.data.clear(); + this.metadata.set('all', true); } - const index = this.data.findIndex(user => user.userId === userId); - if (index !== -1) { - this.data.splice(index, 1, ...users as AdminUserInfo[]); - } else { - this.data.push(...users as AdminUserInfo[]); + for (const user of users) { + this.set(user.userId, user as AdminUserInfo); } - - this.markUpdated(userId); + this.markUpdated(key); return this.data; } + + private isActiveUser(userId: string) { + return this.authInfoService.userInfo?.userId === userId; + } } diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index aef167ef58..a85fd373c7 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -2,14 +2,16 @@ export default [ ['authentication_login_dialog_title', 'Authentication'], ['authentication_login', 'Login'], ['authentication_logout', 'Logout'], + ['authentication_administration_user_info', 'Info'], + ['authentication_administration_user_connections_access', 'Connections Access'], ['authentication_administration_item', 'Users'], ['authentication_administration_confirm_user_deletion', 'Confirm user deletion'], - ['authentication_create_user_dialog_title', 'Create new user'], - ['authentication_create_user_dialog_create', 'Create'], ['authentication_user_name', 'User name'], + ['authentication_user_credentials', 'Credentials'], ['authentication_user_password', 'User password'], ['authentication_user_password_repeat', 'Repeat password'], ['authentication_user_role', 'User role'], + ['authentication_user_password_not_set', 'Password is required'], ['authentication_user_passwords_not_match', "Passwords don't match"], ['authentication_user_user_created', 'User was created successfully'], ]; diff --git a/webapp/packages/plugin-authentication/src/locales/ru.ts b/webapp/packages/plugin-authentication/src/locales/ru.ts index f2ba33e116..ece9be11a9 100644 --- a/webapp/packages/plugin-authentication/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication/src/locales/ru.ts @@ -2,13 +2,16 @@ export default [ ['authentication_login_dialog_title', 'Авторизация'], ['authentication_login', 'Войти'], ['authentication_logout', 'Выйти'], + ['authentication_administration_user_info', 'Общее'], + ['authentication_administration_user_connections_access', 'Доступ к базам'], ['authentication_administration_item', 'Пользователи'], - ['authentication_create_user_dialog_title', 'Создание нового пользователя'], - ['authentication_create_user_dialog_create', 'Создать'], + ['authentication_administration_confirm_user_deletion', 'Подтвердите удаление'], ['authentication_user_name', 'Имя'], + ['authentication_user_credentials', 'Учетные данные'], ['authentication_user_password', 'Пароль'], ['authentication_user_password_repeat', 'Повторите пароль'], ['authentication_user_role', 'Роль'], + ['authentication_user_password_not_set', 'Пароль не задан'], ['authentication_user_passwords_not_match', 'Пароли не совпадают'], ['authentication_user_user_created', 'Пользователь создан успешно'], ]; diff --git a/webapp/packages/plugin-authentication/src/manifest.ts b/webapp/packages/plugin-authentication/src/manifest.ts index 9ac0466371..50476e9bea 100644 --- a/webapp/packages/plugin-authentication/src/manifest.ts +++ b/webapp/packages/plugin-authentication/src/manifest.ts @@ -11,7 +11,6 @@ import { IServiceInjector, PluginManifest } from '@cloudbeaver/core-di'; import { RolesManagerService } from './Administration/RolesManagerService'; import { RolesResource } from './Administration/RolesResource'; import { UsersAdministrationService } from './Administration/Users/UsersAdministrationService'; -import { UsersManagerService } from './Administration/UsersManagerService'; import { UsersResource } from './Administration/UsersResource'; import { AuthenticationLocaleService } from './AuthenticationLocaleService'; import { AuthenticationService } from './AuthenticationService'; @@ -35,7 +34,6 @@ export const manifest: PluginManifest = { AuthDialogService, AuthMenuService, UsersAdministrationService, - UsersManagerService, RolesManagerService, AuthenticationLocaleService, RolesResource,