mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #140 from dbeaver/feat-access-managment
feat(plugin-authentication): rework administration page
This commit is contained in:
@@ -78,10 +78,10 @@ export const TableItem = observer(function TableItem({
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
{isExpanded && ExpandElement && (
|
||||
<tr {...use({ noHover: true })}>
|
||||
<td colSpan={Children.count(children)} {...use({ expandArea: true })}>
|
||||
{ExpandElement && <ExpandElement item={item} />}
|
||||
<ExpandElement item={item} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
+4
-4
@@ -146,19 +146,19 @@ export const ConnectionEdit = observer(function ConnectionEdit({
|
||||
<fill as="div" />
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isSaving}
|
||||
disabled={controller.isDisabled}
|
||||
mod={['outlined']}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{translate('connections_connection_edit_cancel')}
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isSaving}
|
||||
disabled={controller.isDisabled}
|
||||
mod={['unelevated']}
|
||||
onClick={controller.onSaveConnection}
|
||||
>
|
||||
{translate(controller.isNew ? 'connections_connection_edit_add' : 'connections_connection_edit_save')}
|
||||
{translate(controller.isNew ? 'ui_processing_create' : 'ui_processing_save')}
|
||||
</Button>
|
||||
</TabList>
|
||||
{controller.isLoading
|
||||
|
||||
+6
-3
@@ -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) {
|
||||
|
||||
+7
-7
@@ -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'
|
||||
/>
|
||||
</group>
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ export interface IFormController {
|
||||
config: ConnectionConfig;
|
||||
connectionType: ConnectionType;
|
||||
isSaving: boolean;
|
||||
isDisabled: boolean;
|
||||
onChangeType(type: ConnectionType): void;
|
||||
onSelectDriver(driver: DBDriver): void;
|
||||
onChange<T extends keyof ConnectionConfig>(property: T, value: ConnectionConfig[T]): void;
|
||||
|
||||
@@ -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<string, ConnectionInfo> {
|
||||
constructor(
|
||||
private graphQLService: GraphQLService,
|
||||
) {
|
||||
private metadata: MetadataMap<string, boolean>;
|
||||
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<string, ConnectionInf
|
||||
if (id) {
|
||||
this.data.delete(id);
|
||||
}
|
||||
this.data.set(connection.id, connection as ConnectionInfo);
|
||||
this.set(connection.id, connection as ConnectionInfo);
|
||||
|
||||
return this.get(connection.id)!;
|
||||
}
|
||||
@@ -91,6 +106,10 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
}
|
||||
this.markUpdated(key);
|
||||
|
||||
// TODO: getConnections must accept connectionId, so we can update some connection or all connections,
|
||||
// here we should check is it's was a full update
|
||||
this.metadata.set('all', true);
|
||||
|
||||
return this.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
export default [
|
||||
['connections_administration_item', 'Connection Management'],
|
||||
['connections_connection_edit_save', 'Save'],
|
||||
['connections_connection_edit_add', 'Add'],
|
||||
['connections_connection_edit_cancel', 'Cancel'],
|
||||
['connections_connection_edit_authentication', 'Authentication'],
|
||||
['connections_connection_name', 'Name'],
|
||||
['connections_connection_description', 'Description'],
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
export default [
|
||||
['connections_administration_item', 'Управление подключениями'],
|
||||
['connections_connection_edit_save', 'Сохранить'],
|
||||
['connections_connection_edit_add', 'Добавить'],
|
||||
['connections_connection_edit_cancel', 'Отменить'],
|
||||
['connections_connection_edit_authentication', 'Авторизация'],
|
||||
['connections_connection_name', 'Название'],
|
||||
['connections_connection_description', 'Описапние'],
|
||||
|
||||
@@ -6,6 +6,8 @@ export const defaultENLocale = [
|
||||
['ui_processing_canceling', 'Canceling...'],
|
||||
['ui_processing_retry', 'Retry'],
|
||||
['ui_processing_ok', 'Ok'],
|
||||
['ui_processing_create', 'Create'],
|
||||
['ui_processing_save', 'Save'],
|
||||
['ui_error_message', 'Error:'],
|
||||
['ui_errors_details', 'Details'],
|
||||
['ui_search', 'Search...'],
|
||||
|
||||
@@ -6,6 +6,8 @@ export const defaultRULocale = [
|
||||
['ui_processing_canceling', 'Отмена...'],
|
||||
['ui_processing_retry', 'Повторить'],
|
||||
['ui_processing_ok', 'Принять'],
|
||||
['ui_processing_create', 'Создать'],
|
||||
['ui_processing_save', 'Сохранить'],
|
||||
['ui_error_message', 'Ошибка:'],
|
||||
['ui_errors_details', 'Информация'],
|
||||
['ui_search', 'Поиск...'],
|
||||
|
||||
@@ -71,9 +71,9 @@ export abstract class CachedMapResource<TKey, TValue> extends CachedResource<
|
||||
|
||||
isLoaded(key: ResourceKey<TKey>): 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<TKey>): boolean {
|
||||
@@ -142,6 +142,10 @@ export abstract class CachedMapResource<TKey, TValue> extends CachedResource<
|
||||
await this.loadData(key);
|
||||
return this.get(key) as Array<TValue>| TValue;
|
||||
}
|
||||
|
||||
has(key: TKey): boolean {
|
||||
return this.data.has(key);
|
||||
}
|
||||
}
|
||||
|
||||
export function isResourceKeyList<T>(data: any): data is ResourceKeyList<T> {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
query revokeUserRole($userId: ID!, $roleId: ID!) {
|
||||
revokeUserRole(userId: $userId, roleId: $roleId)
|
||||
}
|
||||
@@ -846,6 +846,13 @@ export type GrantUserRoleQueryVariables = Exact<{
|
||||
|
||||
export type GrantUserRoleQuery = Pick<Query, 'grantUserRole'>;
|
||||
|
||||
export type RevokeUserRoleQueryVariables = Exact<{
|
||||
userId: Scalars['ID'];
|
||||
roleId: Scalars['ID'];
|
||||
}>;
|
||||
|
||||
export type RevokeUserRoleQuery = Pick<Query, 'revokeUserRole'>;
|
||||
|
||||
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<GrantUserRoleQuery> {
|
||||
return withWrapper(() => client.request<GrantUserRoleQuery>(GrantUserRoleDocument, variables));
|
||||
},
|
||||
revokeUserRole(variables: RevokeUserRoleQueryVariables): Promise<RevokeUserRoleQuery> {
|
||||
return withWrapper(() => client.request<RevokeUserRoleQuery>(RevokeUserRoleDocument, variables));
|
||||
},
|
||||
setUserCredentials(variables: SetUserCredentialsQueryVariables): Promise<SetUserCredentialsQuery> {
|
||||
return withWrapper(() => client.request<SetUserCredentialsQuery>(SetUserCredentialsDocument, variables));
|
||||
},
|
||||
|
||||
@@ -39,6 +39,14 @@ export class MetadataMap<TKey, TValue> {
|
||||
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)!;
|
||||
|
||||
@@ -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<AdminRoleInfo[], string | undefined> {
|
||||
constructor(
|
||||
private graphQLService: GraphQLService,
|
||||
) {
|
||||
super([]);
|
||||
export class RolesResource extends CachedMapResource<string, AdminRoleInfo> {
|
||||
private metadata: MetadataMap<string, boolean>;
|
||||
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<AdminRoleInfo[]> {
|
||||
async loadAll() {
|
||||
await this.load('all');
|
||||
return this.data;
|
||||
}
|
||||
|
||||
async refreshAll() {
|
||||
await this.refresh('all');
|
||||
return this.data;
|
||||
}
|
||||
|
||||
protected async loader(key: ResourceKey<string>): Promise<Map<string, AdminRoleInfo>> {
|
||||
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;
|
||||
}
|
||||
|
||||
-103
@@ -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))(
|
||||
<>
|
||||
<item-title as='div'>
|
||||
User creation
|
||||
</item-title>
|
||||
<SubmittingForm>
|
||||
<create-form as='div'>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type='text'
|
||||
name='login'
|
||||
value={controller.login}
|
||||
onChange={handleChange}
|
||||
disabled={controller.isCreating}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('User name')}
|
||||
</InputField>
|
||||
</group>
|
||||
<controls as="div">
|
||||
<Button
|
||||
type="button"
|
||||
mod={['unelevated']}
|
||||
onClick={controller.create}
|
||||
disabled={controller.isCreating}
|
||||
>
|
||||
{translate('Create')}
|
||||
</Button>
|
||||
</controls>
|
||||
</create-form>
|
||||
</SubmittingForm>
|
||||
|
||||
{controller.error.responseMessage && (
|
||||
<ErrorMessage
|
||||
text={controller.error.responseMessage}
|
||||
hasDetails={controller.error.hasDetails}
|
||||
onShowDetails={controller.showDetails}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
-57
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-161
@@ -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<null, null>) {
|
||||
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))(
|
||||
<CommonDialogWrapper
|
||||
title={translate('authentication_create_user_dialog_title')}
|
||||
noBodyPadding
|
||||
footer={(
|
||||
<CreateUserDialogFooter
|
||||
isFormFilled={controller.isFormFilled}
|
||||
isCreating={controller.isCreating}
|
||||
onCancel={rejectDialog}
|
||||
onCreate={controller.create}
|
||||
/>
|
||||
)}
|
||||
onReject={rejectDialog}
|
||||
>
|
||||
<SubmittingForm onSubmit={controller.create} autoComplete="disabled" ref={focusedRef as React.RefObject<HTMLFormElement>}>
|
||||
<create-form as='div'>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type='text'
|
||||
name='login'
|
||||
value={controller.credentials.login}
|
||||
onChange={handleLoginChange}
|
||||
disabled={controller.isCreating}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('authentication_user_name')}
|
||||
</InputField>
|
||||
</group>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type='password'
|
||||
name='password'
|
||||
value={controller.credentials.password}
|
||||
onChange={handlePasswordChange}
|
||||
disabled={controller.isCreating}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('authentication_user_password')}
|
||||
</InputField>
|
||||
</group>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type='password'
|
||||
name='password_repeat'
|
||||
value={controller.credentials.passwordRepeat}
|
||||
onChange={handlePasswordRepeatChange}
|
||||
disabled={controller.isCreating}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('authentication_user_password_repeat')}
|
||||
</InputField>
|
||||
</group>
|
||||
{controller.roles.map((role, i) => (
|
||||
<group as="div" key={role.roleId}>
|
||||
<Checkbox
|
||||
value={role.roleId}
|
||||
name='role'
|
||||
checkboxLabel={role.roleName || role.roleId}
|
||||
onChange={checked => handleRoleChange(role.roleId, checked)}
|
||||
checked={controller.credentials.roles.get(role.roleId)}
|
||||
disabled={controller.isCreating}
|
||||
mod='surface'
|
||||
>
|
||||
{i === 0 && translate('authentication_user_role')}
|
||||
</Checkbox>
|
||||
</group>
|
||||
))}
|
||||
</create-form>
|
||||
</SubmittingForm>
|
||||
{controller.error.responseMessage && (
|
||||
<ErrorMessage
|
||||
text={controller.error.responseMessage}
|
||||
hasDetails={controller.error.hasDetails}
|
||||
onShowDetails={controller.showDetails}
|
||||
/>
|
||||
)}
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
});
|
||||
-68
@@ -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)(
|
||||
<controls as="div">
|
||||
<Button
|
||||
type="button"
|
||||
mod={['outlined']}
|
||||
onClick={onCancel}
|
||||
disabled={isCreating}
|
||||
>
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
<fill as="div"/>
|
||||
<Button
|
||||
type="button"
|
||||
mod={['unelevated']}
|
||||
onClick={onCreate}
|
||||
disabled={!isFormFilled || isCreating}
|
||||
>
|
||||
{translate('authentication_create_user_dialog_create')}
|
||||
</Button>
|
||||
</controls>
|
||||
);
|
||||
}
|
||||
);
|
||||
+5
-1
@@ -57,7 +57,11 @@ export const UsersAdministration = observer(function UsersAdministration() {
|
||||
<IconButton name="reload" viewBox="0 0 28 28" onClick={controller.update} />
|
||||
</AdministrationTools>
|
||||
<content as='div'>
|
||||
<UsersTable users={controller.users} selectedItems={controller.selectedItems} />
|
||||
<UsersTable
|
||||
users={controller.users}
|
||||
selectedItems={controller.selectedItems}
|
||||
expandedItems={controller.expandedItems}
|
||||
/>
|
||||
{controller.isLoading && <Loader overlay/>}
|
||||
</content>
|
||||
</>
|
||||
|
||||
+20
-9
@@ -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<string, boolean>(new Map())
|
||||
readonly expandedItems = observable<string, boolean>(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');
|
||||
|
||||
+3
-12
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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())(
|
||||
<TableItem item={user.userId}>
|
||||
<TableColumnValue centerContent><TableItemSelect /></TableColumnValue>
|
||||
const usersResource = useService(UsersResource);
|
||||
const isNew = usersResource.isNew(user.userId);
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<TableItem item={user.userId} expandElement={UserEdit}>
|
||||
<TableColumnValue centerContent flex {...use({ new: isNew })}>
|
||||
<TableItemSelect />
|
||||
<TableItemExpand />
|
||||
</TableColumnValue>
|
||||
<TableColumnValue>{user.userId}</TableColumnValue>
|
||||
<TableColumnValue>{user.grantedRoles.join(', ')}</TableColumnValue>
|
||||
<TableColumnValue></TableColumnValue>
|
||||
|
||||
+266
@@ -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))(
|
||||
<TabsState selectedId='info'>
|
||||
<TabList>
|
||||
<Tab tabId='info' >
|
||||
<TabTitle>{translate('authentication_administration_user_info')}</TabTitle>
|
||||
</Tab>
|
||||
<Tab tabId='connections_access' >
|
||||
<TabTitle>{translate('authentication_administration_user_connections_access')}</TabTitle>
|
||||
</Tab>
|
||||
<fill as="div" />
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isCreating}
|
||||
mod={['outlined']}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={controller.isCreating}
|
||||
mod={['unelevated']}
|
||||
onClick={controller.save}
|
||||
>
|
||||
{translate(controller.isNew ? 'ui_processing_create' : 'ui_processing_save')}
|
||||
</Button>
|
||||
</TabList>
|
||||
{controller.isLoading
|
||||
? <Loader />
|
||||
: (
|
||||
<SubmittingForm onSubmit={controller.save} ref={focusedRef as React.RefObject<HTMLFormElement>}>
|
||||
<layout-grid as="div">
|
||||
<layout-grid-inner as="div">
|
||||
<layout-grid-cell as='div' {...use({ 'span-tablet': 12, 'span-desktop': 5 })}>
|
||||
<group as="div">
|
||||
<InputGroup>{translate('authentication_user_credentials')}</InputGroup>
|
||||
</group>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type='text'
|
||||
name='login'
|
||||
value={controller.credentials.login}
|
||||
onChange={handleLoginChange}
|
||||
disabled={!controller.isNew || controller.isCreating}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('authentication_user_name')}
|
||||
</InputField>
|
||||
</group>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type='password'
|
||||
name='password'
|
||||
value={controller.credentials.password}
|
||||
onChange={handlePasswordChange}
|
||||
disabled={controller.isCreating}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('authentication_user_password')}
|
||||
</InputField>
|
||||
</group>
|
||||
<group as="div">
|
||||
<InputField
|
||||
type='password'
|
||||
name='password_repeat'
|
||||
value={controller.credentials.passwordRepeat}
|
||||
onChange={handlePasswordRepeatChange}
|
||||
disabled={controller.isCreating}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('authentication_user_password_repeat')}
|
||||
</InputField>
|
||||
</group>
|
||||
</layout-grid-cell>
|
||||
<layout-grid-cell as='div' {...use({ 'span-tablet': 12, 'span-desktop': 5 })}>
|
||||
<group as="div">
|
||||
<InputGroup>{translate('authentication_user_role')}</InputGroup>
|
||||
</group>
|
||||
{controller.roles.map((role, i) => (
|
||||
<group as="div" key={role.roleId}>
|
||||
<Checkbox
|
||||
value={role.roleId}
|
||||
name='role'
|
||||
checkboxLabel={role.roleName || role.roleId}
|
||||
onChange={checked => handleRoleChange(role.roleId, checked)}
|
||||
checked={controller.credentials.roles.get(role.roleId)}
|
||||
disabled={controller.isCreating}
|
||||
mod='surface'
|
||||
/>
|
||||
</group>
|
||||
))}
|
||||
</layout-grid-cell>
|
||||
</layout-grid-inner>
|
||||
</layout-grid>
|
||||
</SubmittingForm>
|
||||
)}
|
||||
{controller.error.responseMessage && (
|
||||
<ErrorMessage
|
||||
text={controller.error.responseMessage}
|
||||
hasDetails={controller.error.hasDetails}
|
||||
onShowDetails={controller.showDetails}
|
||||
/>
|
||||
)}
|
||||
</TabsState>
|
||||
);
|
||||
});
|
||||
+55
-17
@@ -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<string, boolean>(),
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-11
@@ -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<string, boolean>;
|
||||
expandedItems: Map<string, boolean>;
|
||||
}
|
||||
|
||||
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))(
|
||||
<Table selectedItems={selectedItems}>
|
||||
<Table selectedItems={selectedItems} expandedItems={expandedItems}>
|
||||
<TableHeader>
|
||||
<TableColumnHeader min/>
|
||||
<TableColumnHeader>{translate('authentication_user_name')}</TableColumnHeader>
|
||||
|
||||
@@ -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<AdminUserInfo> {
|
||||
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<string, any>) {
|
||||
const provider = 'local';
|
||||
const processedCredentials = await this.authProviderService.processCredentials(provider, credentials);
|
||||
|
||||
await this.graphQLService.gql.setUserCredentials({
|
||||
providerId: provider,
|
||||
userId,
|
||||
credentials: processedCredentials,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<AdminUserInfo[], string | undefined> {
|
||||
export class UsersResource extends CachedMapResource<string, AdminUserInfo> {
|
||||
private metadata: MetadataMap<string, boolean>;
|
||||
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<AdminUserInfo[]> {
|
||||
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<AdminUserInfo> {
|
||||
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<string, any>) {
|
||||
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<string>) {
|
||||
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<string>): Promise<Map<string, AdminUserInfo>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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', 'Пользователь создан успешно'],
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user