diff --git a/webapp/packages/core-blocks/src/InfoItem.tsx b/webapp/packages/core-blocks/src/InfoItem.tsx new file mode 100644 index 0000000000..fc4ddd3765 --- /dev/null +++ b/webapp/packages/core-blocks/src/InfoItem.tsx @@ -0,0 +1,46 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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-lite'; +import styled, { css } from 'reshadow'; + +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; + +import { IconOrImage } from './IconOrImage'; + +export interface IInfoItem { + info: TLocalizationToken; + icon?: string; +} + +interface Props extends IInfoItem { + className?: string; +} + +const styles = css` + info-item { + display: flex; + align-items: center; + flex: 0 0 auto; + } + IconOrImage { + width: 24px; + height: 24px; + margin-right: 16px; + } +`; + +export const InfoItem = observer(function InfoItem({ info, icon = '/icons/info_icon.svg', className }) { + const translate = useTranslate(); + return styled(styles)( + + + {translate(info)} + + ); +}); diff --git a/webapp/packages/core-blocks/src/Table/TableState.ts b/webapp/packages/core-blocks/src/Table/TableState.ts new file mode 100644 index 0000000000..b53a970131 --- /dev/null +++ b/webapp/packages/core-blocks/src/Table/TableState.ts @@ -0,0 +1,74 @@ + +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { action, computed, makeObservable, observable } from 'mobx'; + +type Key = string | string[]; + +export class TableState { + selected: Map; + expanded: Map; + + get itemsSelected(): boolean { + return Array.from(this.selected.values()).some(v => v); + } + + get selectedList(): string[] { + return Array + .from(this.selected) + .filter(([_, value]) => value) + .map(([key]) => key); + } + + constructor() { + this.selected = new Map(); + this.expanded = new Map(); + + makeObservable(this, { + selected: observable, + expanded: observable, + itemsSelected: computed, + selectedList: computed, + unselect: action, + unexpand: action, + }); + } + + unselect(key?: Key): Map { + if (key === undefined) { + this.selected.clear(); + } else { + if (typeof key === 'string') { + this.selected.delete(key); + } else { + for (const id of key) { + this.selected.delete(id); + } + } + } + + return this.selected; + } + + unexpand(key?: Key): Map { + if (key === undefined) { + this.expanded.clear(); + } else { + if (typeof key === 'string') { + this.expanded.delete(key); + } else { + for (const id of key) { + this.expanded.delete(id); + } + } + } + + return this.expanded; + } +} diff --git a/webapp/packages/core-blocks/src/Table/useTable.tsx b/webapp/packages/core-blocks/src/Table/useTable.tsx index 9fcd74b2b5..2472a1adb9 100644 --- a/webapp/packages/core-blocks/src/Table/useTable.tsx +++ b/webapp/packages/core-blocks/src/Table/useTable.tsx @@ -7,70 +7,11 @@ * you may not use this file except in compliance with the License. */ -import { action, computed, observable } from 'mobx'; +import { useState } from 'react'; -import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { TableState } from './TableState'; -type Key = string | string[]; - -export interface ITableState { - selected: Map; - expanded: Map; - readonly itemsSelected: boolean; - readonly selectedList: string[]; - unselect: (key?: Key) => Map; - unexpand: (key?: Key) => Map; -} - -export function useTable(): ITableState { - return useObservableRef(() => ({ - selected: new Map(), - expanded: new Map(), - get selectedList() { - return Array - .from(this.selected) - .filter(([_, value]) => value) - .map(([key]) => key); - }, - get itemsSelected() { - return Array.from(this.selected.values()).some(v => v); - }, - unselect(key?: string | string[]) { - if (key === undefined) { - this.selected.clear(); - } else { - if (typeof key === 'string') { - this.selected.delete(key); - } else { - for (const id of key) { - this.selected.delete(id); - } - } - } - - return this.selected; - }, - unexpand(key?: string | string[]) { - if (key === undefined) { - this.expanded.clear(); - } else { - if (typeof key === 'string') { - this.expanded.delete(key); - } else { - for (const id of key) { - this.expanded.delete(id); - } - } - } - - return this.expanded; - }, - }), { - selected: observable, - expanded: observable, - selectedList: computed, - itemsSelected: computed, - unexpand: action.bound, - unselect: action.bound, - }, false); +export function useTable(): TableState { + const [table] = useState(() => new TableState()); + return table; } diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index ba9c605d48..663c1083ed 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -52,6 +52,7 @@ export * from './Table/TableItemExpand'; export * from './Table/TableItemSelect'; export * from './Table/TableItemSeparator'; export * from './Table/useTable'; +export * from './Table/TableState'; export * from './Tabs/ITab'; export * from './Tabs/TabContext'; @@ -139,6 +140,7 @@ export * from './IconButton'; export * from './IconOrImage'; export * from './StaticImage'; export * from './TextPlaceholder'; +export * from './InfoItem'; export * from './useClickEvents'; export * from './useClipboard'; export * from './useDataResource'; diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx index 5690343916..2a59ab6ed1 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx @@ -21,8 +21,8 @@ import { BASE_CONTAINERS_STYLES, ColoredContainer, Group, - IconOrImage, Container, + InfoItem, } from '@cloudbeaver/core-blocks'; import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -44,35 +44,20 @@ const styles = css` position: relative; overflow: auto !important; } - info-item { - display: flex; - align-items: center; - flex: 0 0 auto; - } - IconOrImage { - width: 24px; - height: 24px; - margin-right: 16px; - } Loader { z-index: 2; } `; -interface IInfoItem { - text: TLocalizationToken; - icon: string; -} - export const ConnectionAccess: TabContainerPanelComponent = observer(function ConnectionAccess({ tabId, state: formState, }) { - const { state, edit, grant, revoke, load } = useConnectionAccessState(formState.info); + const state = useConnectionAccessState(formState.info); const style = useStyles(styles, BASE_CONTAINERS_STYLES); const translate = useTranslate(); - const unsaved = state.initialGrantedSubjects.length !== state.grantedSubjects.length - || state.initialGrantedSubjects.some(subject => !state.grantedSubjects.includes(subject)); + + const { selected } = useTab(tabId, state.load); const users = useMapResource(UsersResource, null, { onLoad: resource => { resource.loadAll(); }, @@ -83,39 +68,30 @@ export const ConnectionAccess: TabContainerPanelComponent }); const grantedUsers = useMemo(() => computed(() => users.resource.values - .filter(user => state.grantedSubjects.includes(user.userId)) - ), [state.grantedSubjects, users.resource]); + .filter(user => state.state.grantedSubjects.includes(user.userId)) + ), [state.state.grantedSubjects, users.resource]); const grantedRoles = useMemo(() => computed(() => roles.resource.values - .filter(role => state.grantedSubjects.includes(role.roleId)) - ), [state.grantedSubjects, roles.resource]); - - const { selected } = useTab(tabId, load); - const loading = users.isLoading() || roles.isLoading() || state.loading; - const cloud = formState.info ? isCloudConnection(formState.info) : false; - const disabled = loading || !state.loaded || formState.disabled || cloud; - let infoItem: IInfoItem | null = null; - - if (unsaved) { - infoItem = { - text: 'connections_connection_access_save_reminder', - icon: '/icons/info_icon.svg', - }; - } - - if (cloud) { - infoItem = { - text: 'connections_connection_access_cloud_placeholder', - icon: '/icons/info_icon.svg', - }; - } + .filter(role => state.state.grantedSubjects.includes(role.roleId)) + ), [state.state.grantedSubjects, roles.resource]); if (!selected) { return null; } + const loading = users.isLoading() || roles.isLoading() || state.state.loading; + const cloud = formState.info ? isCloudConnection(formState.info) : false; + const disabled = loading || !state.state.loaded || formState.disabled || cloud; + let info: TLocalizationToken | null = null; + + if (formState.mode === 'edit' && state.changed) { + info = 'ui_save_reminder'; + } else if (cloud) { + info = 'cloud_connections_access_placeholder'; + } + return styled(style)( - + {() => styled(style)( {!users.resource.values.length && !roles.resource.values.length ? ( @@ -124,27 +100,22 @@ export const ConnectionAccess: TabContainerPanelComponent ) : ( <> - {infoItem && ( - - - {translate(infoItem.text)} - - )} + {info && } - {state.editing && ( + {state.state.editing && ( )} diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessGrantedList.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessGrantedList.tsx index 5780584929..53fc8016bb 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessGrantedList.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessGrantedList.tsx @@ -98,9 +98,9 @@ export const ConnectionAccessGrantedList = observer(function ConnectionAc let tableInfoText: TLocalizationToken = 'connections_connection_access_admin_info'; if (!roles.get().length && !users.get().length) { if (filterState.filterValue) { - tableInfoText = 'connections_connection_access_filter_no_result'; + tableInfoText = 'ui_search_no_result_placeholder'; } else { - tableInfoText = 'connections_connection_access_empty_table_placeholder'; + tableInfoText = 'ui_no_items_placeholder'; } } @@ -127,7 +127,7 @@ export const ConnectionAccessGrantedList = observer(function ConnectionAc name={role.roleName || ''} description={role.description} icon='/icons/role.svg' - iconTooltip={translate('connections_connection_access_role_tooltip')} + iconTooltip={translate('authentication_role_icon_tooltip')} disabled={disabled} /> ))} @@ -137,7 +137,7 @@ export const ConnectionAccessGrantedList = observer(function ConnectionAc id={user.userId} name={user.userId} icon='/icons/user.svg' - iconTooltip={translate('connections_connection_access_user_tooltip')} + iconTooltip={translate('authentication_user_icon_tooltip')} disabled={disabled} /> ))} diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx index 85ba0f2293..c4f8f74e6b 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx @@ -108,7 +108,7 @@ export const ConnectionAccessList = observer(function ConnectionAccessLis {!roles.get().length && !users.get().length && filterState.filterValue && ( - {translate('connections_connection_access_filter_no_result')} + {translate('ui_search_no_result_placeholder')} )} @@ -119,7 +119,7 @@ export const ConnectionAccessList = observer(function ConnectionAccessLis name={role.roleName || ''} description={role.description} icon='/icons/role.svg' - iconTooltip='connections_connection_access_role_tooltip' + iconTooltip='authentication_role_icon_tooltip' disabled={disabled || grantedSubjects.includes(role.roleId)} /> ))} @@ -129,7 +129,7 @@ export const ConnectionAccessList = observer(function ConnectionAccessLis id={user.userId} name={user.userId} icon='/icons/user.svg' - iconTooltip={translate('connections_connection_access_user_tooltip')} + iconTooltip={translate('authentication_user_icon_tooltip')} disabled={disabled || grantedSubjects.includes(user.userId)} /> ))} diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts index 3c26bc010b..0baacf1067 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts @@ -6,59 +6,67 @@ * you may not use this file except in compliance with the License. */ -import { useObjectRef, useTabState } from '@cloudbeaver/core-blocks'; +import { action, computed, observable } from 'mobx'; + +import { useObservableRef, useTabState } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; import { ConnectionsResource } from '../../Administration/ConnectionsResource'; import type { IConnectionAccessTabState } from './IConnectionAccessTabState'; -interface IConnectionAccessState { +interface State { state: IConnectionAccessTabState; + changed: boolean; + edit: () => void; revoke: (subjectIds: string[]) => void; grant: (subjectIds: string[]) => void; - edit: () => void; load: () => Promise; } -export function useConnectionAccessState(connection: DatabaseConnectionFragment | undefined): IConnectionAccessState { - const connectionsResource = useService(ConnectionsResource); +export function useConnectionAccessState(connection: DatabaseConnectionFragment | undefined): Readonly { + const resource = useService(ConnectionsResource); const notificationService = useService(NotificationService); const state = useTabState(); - const edit = () => { - state.editing = !state.editing; - }; - - const revoke = (subjectIds: string[]): void => { - state.grantedSubjects = state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); - }; - - const grant = (subjectIds: string[]): void => { - state.grantedSubjects.push(...subjectIds); - }; - - const load = async () => { - if (state.loaded) { - return; - } - - try { - state.loading = true; - - if (connection) { - const grantedSubjects = await connectionsResource.loadAccessSubjects(connection.id); - state.grantedSubjects = grantedSubjects.map(subject => subject.subjectId); - state.initialGrantedSubjects = state.grantedSubjects.slice(); + return useObservableRef(() => ({ + get changed() { + return !isArraysEqual(this.state.initialGrantedSubjects, this.state.grantedSubjects); + }, + edit() { + this.state.editing = !this.state.editing; + }, + revoke(subjectIds: string[]) { + this.state.grantedSubjects = this.state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); + }, + grant(subjectIds: string[]) { + this.state.grantedSubjects.push(...subjectIds); + }, + async load() { + if (this.state.loaded || this.state.loading) { + return; } - state.loaded = true; - } catch (exception) { - notificationService.logException(exception, 'connections_connection_edit_access_load_failed'); - } - state.loading = false; - }; + try { + this.state.loading = true; - return useObjectRef({ state, revoke, grant, edit, load }); + if (this.connection) { + const grantedSubjects = await this.resource.loadAccessSubjects(this.connection.id); + this.state.grantedSubjects = grantedSubjects.map(subject => subject.subjectId); + this.state.initialGrantedSubjects = this.state.grantedSubjects.slice(); + } + + this.state.loaded = true; + } catch (exception) { + this.notificationService.logException(exception, 'connections_connection_edit_access_load_failed'); + } finally { + this.state.loading = false; + } + }, + }), + { state: observable.ref, changed: computed, edit: action.bound, revoke: action.bound, grant: action.bound }, + { state, connection, resource, notificationService }, + ['load']); } diff --git a/webapp/packages/core-connections/src/locales/en.ts b/webapp/packages/core-connections/src/locales/en.ts index 0a56c931e9..9da755c109 100644 --- a/webapp/packages/core-connections/src/locales/en.ts +++ b/webapp/packages/core-connections/src/locales/en.ts @@ -34,14 +34,8 @@ export default [ ['connections_connection_address', 'Address'], ['connections_connection_name', 'Connection name'], ['connections_connection_access_user_or_role_name', 'User or Role name'], - ['connections_connection_access_save_reminder', 'You have unsaved changes. Click Save to save all permission changes.'], - ['connections_connection_access_user_tooltip', 'User'], - ['connections_connection_access_role_tooltip', 'Role'], - ['connections_connection_access_cloud_placeholder', 'Cloud connections are visible to all users. Access management can be configured in "AWS Management Console".'], - ['connections_connection_access_empty_table_placeholder', 'There are no items yet. Press Edit button to manage access settings.'], ['connections_connection_access_filter_placeholder', 'Search for user or role name'], - ['connections_connection_access_filter_no_result', 'No results have been found.'], - ['connections_connection_access_admin_info', 'Administators see all connections.'], + ['connections_connection_access_admin_info', 'Administrators see all connections.'], ['connections_connection_description', 'Description'], ['connections_connection_driver', 'Driver'], ['connections_connection_host', 'Host'], @@ -65,4 +59,6 @@ export default [ ['connections_network_handler_ssh_tunnel_password', 'Password'], ['connections_driver_search_placeholder', 'Type driver name...'], ['connections_not_found', 'No database connections were found'], + + ['cloud_connections_access_placeholder', 'Cloud connections are visible to all users. Access management can be configured in "AWS Management Console".'], ]; diff --git a/webapp/packages/core-connections/src/locales/it.ts b/webapp/packages/core-connections/src/locales/it.ts index e26c300d22..026b44b8fd 100644 --- a/webapp/packages/core-connections/src/locales/it.ts +++ b/webapp/packages/core-connections/src/locales/it.ts @@ -34,16 +34,10 @@ export default [ ['connections_connection_address', 'Indirizzo'], ['connections_connection_name', 'Nome della connessione'], ['connections_connection_access_user_or_role_name', 'Nome dell\'Utente o del ruolo'], - ['connections_connection_access_save_reminder', 'Ci sono modifiche non salvate. Cliccare Salva per salvare tutte le modifiche ai permessi.'], - ['connections_connection_access_user_tooltip', 'Utente'], - ['connections_connection_access_role_tooltip', 'Ruolo'], ['connections_connection_access_revoke', 'Revoca'], ['connections_connection_access_grant', 'Permetti'], ['connections_connection_access_edit', 'Modifica'], - ['connections_connection_access_cloud_placeholder', 'Connessioni Cloud sono visibili a tutti gli utenti. Il gestore degli accessi può essere configurato in "AWS Management Console".'], - ['connections_connection_access_empty_table_placeholder', 'Non ci sono ancora elementi. Premi il pulsante Modifica per gestire le impostazioni d\'accesso.'], ['connections_connection_access_filter_placeholder', 'Cerca per utente o ruolo'], - ['connections_connection_access_filter_no_result', 'Nessun risultato trovato.'], ['connections_connection_access_admin_info', 'Gli amministratori vedono tutte le connessioni.'], ['connections_connection_description', 'Descrizione'], ['connections_connection_driver', 'Driver'], @@ -68,4 +62,6 @@ export default [ ['connections_network_handler_ssh_tunnel_password', 'Password'], ['connections_driver_search_placeholder', 'Digita il nome del driver...'], ['connections_not_found', 'Nessuna connessione al database trovata'], + + ['cloud_connections_access_placeholder', 'Connessioni Cloud sono visibili a tutti gli utenti. Il gestore degli accessi può essere configurato in "AWS Management Console".'], ]; diff --git a/webapp/packages/core-connections/src/locales/ru.ts b/webapp/packages/core-connections/src/locales/ru.ts index 0fddff65c8..14dfd3abf0 100644 --- a/webapp/packages/core-connections/src/locales/ru.ts +++ b/webapp/packages/core-connections/src/locales/ru.ts @@ -31,13 +31,7 @@ export default [ ['connections_connection_address', 'Адрес'], ['connections_connection_name', 'Название подключения'], ['connections_connection_access_user_or_role_name', 'Имя пользователя или роли'], - ['connections_connection_access_save_reminder', 'У вас есть несохраненные изменения. Нажмите кнопку Сохранить, чтобы применить изменения в настройках доступа.'], - ['connections_connection_access_user_tooltip', 'Пользователь'], - ['connections_connection_access_role_tooltip', 'Роль'], - ['connections_connection_access_cloud_placeholder', 'Облачные подключения видны всем пользователям. Вы можете настроить доступ в "Консоль Управления AWS".'], - ['connections_connection_access_empty_table_placeholder', 'Вы еще ничего не добавили. Нажмите кнопку Изменить, чтобы изменить настройки доступа.'], ['connections_connection_access_filter_placeholder', 'Поиск по имени пользователя или роли'], - ['connections_connection_access_filter_no_result', 'По вашему запросу ничего не найдено.'], ['connections_connection_access_admin_info', 'Администраторы видят все подключения.'], ['connections_connection_description', 'Описание'], ['connections_connection_driver', 'Драйвер'], @@ -62,4 +56,6 @@ export default [ ['connections_network_handler_ssh_tunnel_password', 'Пароль'], ['connections_driver_search_placeholder', 'Введите название драйвера...'], ['connections_not_found', 'Подключения к базам данных не найдены'], + + ['cloud_connections_access_placeholder', 'Облачные подключения видны всем пользователям. Вы можете настроить доступ в "Консоль Управления AWS".'], ]; diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 6f5ce6bf7a..202d67bd1a 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -41,6 +41,9 @@ export const defaultENLocale = [ ['ui_rename_processing', 'Renaming...'], ['ui_name', 'Name'], ['ui_cant_delete_item', "This item can't be deleted"], + ['ui_no_items_placeholder', 'There are no items yet.'], + ['ui_search_no_result_placeholder', 'No results have been found.'], + ['ui_save_reminder', 'You have unsaved changes.'], ['root_permission_denied', 'You have no permissions'], ['app_root_session_expire_warning_title', 'Session is about to expire'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index 9125403449..241a49e4d9 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -34,6 +34,10 @@ export const defaultITLocale = [ ['ui_clipboard_access_denied_message', 'Devi autorizzare l\'accesso agli appunti per utilizzare alcune feature dell\'app'], ['ui_reveal_password', 'Mostra o nascondi la password'], ['ui_page_not_found', 'Pagina non trovata'], + ['ui_no_items_placeholder', 'Non ci sono ancora elementi.'], + ['ui_search_no_result_placeholder', 'Nessun risultato trovato.'], + ['ui_save_reminder', 'Ci sono modifiche non salvate.'], + ['root_permission_denied', 'Non hai i permessi'], ['app_root_session_expire_warning_title', 'La sessione sta per scadere'], ['app_root_session_expire_warning_message', 'La sessione scadrà in meno di 5 minuti. Per continuare a lavorare, chiudi questo popup.'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 2690edb7a0..3a5d6473a7 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -39,6 +39,9 @@ export const defaultRULocale = [ ['ui_rename_processing', 'Переименование...'], ['ui_name', 'Название'], ['ui_cant_delete_item', 'Этот элемент нельзя удалить'], + ['ui_no_items_placeholder', 'Вы еще ничего не добавили.'], + ['ui_search_no_result_placeholder', 'По вашему запросу ничего не найдено.'], + ['ui_save_reminder', 'У вас есть несохраненные изменения.'], ['root_permission_denied', 'Отказано в доступе'], ['app_root_session_expire_warning_title', 'Ваша сессия скоро закончится'], diff --git a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx index bf2a17700d..377ce72e95 100644 --- a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css, use } from 'reshadow'; import { AdministrationItemContentComponent, ADMINISTRATION_TOOLS_PANEL_STYLES } from '@cloudbeaver/core-administration'; -import { BASE_CONTAINERS_STYLES, ToolsAction, Loader, ToolsPanel, useTable } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, ToolsAction, Loader, ToolsPanel } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; @@ -51,8 +51,7 @@ export const AuthConfigurationsAdministration: AdministrationItemContentComponen const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES, BASE_CONTAINERS_STYLES); const service = useService(CreateAuthConfigurationService); - const tableState = useTable(); - const configurationsTableState = useConfigurationsTable(tableState); + const table = useConfigurationsTable(); return styled(style)( <> @@ -61,7 +60,7 @@ export const AuthConfigurationsAdministration: AdministrationItemContentComponen title={translate('administration_identity_providers_add_tooltip')} icon="add" viewBox="0 0 24 24" - disabled={!!sub || configurationsTableState.processing} + disabled={!!sub || table.processing} onClick={service.create} > {translate('ui_add')} @@ -70,8 +69,8 @@ export const AuthConfigurationsAdministration: AdministrationItemContentComponen title={translate('administration_identity_providers_refresh_tooltip')} icon="refresh" viewBox="0 0 24 24" - disabled={configurationsTableState.processing} - onClick={configurationsTableState.update} + disabled={table.processing} + onClick={table.update} > {translate('ui_refresh')} @@ -79,8 +78,8 @@ export const AuthConfigurationsAdministration: AdministrationItemContentComponen title={translate('administration_identity_providers_delete_tooltip')} icon="trash" viewBox="0 0 24 24" - disabled={!tableState.itemsSelected || configurationsTableState.processing} - onClick={configurationsTableState.delete} + disabled={!table.tableState.itemsSelected || table.processing} + onClick={table.delete} > {translate('ui_delete')} @@ -94,11 +93,11 @@ export const AuthConfigurationsAdministration: AdministrationItemContentComponen )} - + diff --git a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx index 5bf449d539..7554cdedda 100644 --- a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx @@ -9,28 +9,28 @@ import { computed, observable } from 'mobx'; import { AuthConfigurationsResource, compareAuthConfigurations } from '@cloudbeaver/core-authentication'; -import { ITableState, useObservableRef } from '@cloudbeaver/core-blocks'; +import { TableState, useObservableRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { AdminAuthProviderConfiguration, resourceKeyList } from '@cloudbeaver/core-sdk'; interface State { + tableState: TableState; processing: boolean; - tableState: ITableState; configurations: AdminAuthProviderConfiguration[]; update: () => Promise; delete: () => Promise; } -export function useConfigurationsTable(tableState: ITableState): State { +export function useConfigurationsTable(): Readonly { const notificationService = useService(NotificationService); const dialogService = useService(CommonDialogService); const resource = useService(AuthConfigurationsResource); return useObservableRef(() => ({ + tableState: new TableState(), processing: false, - tableState, get configurations() { return resource.values.slice().sort(compareAuthConfigurations); }, @@ -89,5 +89,5 @@ export function useConfigurationsTable(tableState: ITableState): State { }), { processing: observable.ref, configurations: computed, - }, { tableState }, ['update', 'delete']); + }, false, ['update', 'delete']); } diff --git a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx index 869b467a66..cecd5fb9ba 100644 --- a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx @@ -153,8 +153,6 @@ export const AuthConfigurationOptions: TabContainerPanelComponent Sign in @@ -164,8 +162,6 @@ export const AuthConfigurationOptions: TabContainerPanelComponent Sign out diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/ConnectionList.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/ConnectionList.tsx index 2935357ef9..6617c5f3a6 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/ConnectionList.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/ConnectionList.tsx @@ -63,15 +63,15 @@ const styles = composes( interface Props { connectionList: DatabaseConnectionFragment[]; grantedSubjects: string[]; - onGrant: (subjectIds: string[]) => void; disabled: boolean; + onGrant: (subjectIds: string[]) => void; } export const ConnectionList = observer(function ConnectionList({ connectionList, grantedSubjects, - onGrant, disabled, + onGrant, }) { const props = useObjectRef({ onGrant }); const style = useStyles(styles, BASE_CONTAINERS_STYLES); @@ -105,7 +105,7 @@ export const ConnectionList = observer(function ConnectionList({ {!connections.length && filterState.filterValue && ( - {translate('connections_connection_access_filter_no_result')} + {translate('ui_search_no_result_placeholder')} )} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnections.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnections.tsx index 47fcf450a5..501089b3a8 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnections.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnections.tsx @@ -13,7 +13,7 @@ import styled, { css } from 'reshadow'; import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, - IconOrImage, Loader, TabContainerPanelComponent, + InfoItem, Loader, TabContainerPanelComponent, TextPlaceholder, useMapResource, useTab, } from '@cloudbeaver/core-blocks'; import { ConnectionsResource, DBDriverResource, isCloudConnection } from '@cloudbeaver/core-connections'; @@ -36,26 +36,11 @@ const styles = css` position: relative; overflow: auto !important; } - info-item { - display: flex; - align-items: center; - flex: 0 0 auto; - } - IconOrImage { - width: 24px; - height: 24px; - margin-right: 16px; - } Loader { z-index: 2; } `; -interface IInfoItem { - text: TLocalizationToken; - icon: string; -} - export const GrantedConnections: TabContainerPanelComponent = observer(function GrantedConnections({ tabId, state: formState, @@ -63,49 +48,40 @@ export const GrantedConnections: TabContainerPanelComponent = ob const style = useStyles(BASE_CONTAINERS_STYLES, styles); const translate = useTranslate(); - const { state, edit, grant, load, revoke } = useGrantedConnections(formState.config, formState.mode); - + const state = useGrantedConnections(formState.config, formState.mode); const { selected } = useTab(tabId); const dbDriverResource = useMapResource(DBDriverResource, selected ? 'all' : null); const connections = useMapResource(ConnectionsResource, selected ? ConnectionsResource.keyAll : null); const grantedConnections = useMemo(() => computed(() => connections.resource.values - .filter(connection => state.grantedSubjects.includes(connection.id)) - ), [state.grantedSubjects, connections.resource]); + .filter(connection => state.state.grantedSubjects.includes(connection.id)) + ), [state.state.grantedSubjects, connections.resource]); useEffect(() => { - if (selected && !state.loaded) { - load(); + if (selected && !state.state.loaded) { + state.load(); } - }, [selected, state.loaded, load]); + }, [selected, state.state.loaded]); if (!selected) { return null; } - let infoItem: IInfoItem | null = null; + let info: TLocalizationToken | null = null; - const unsaved = (formState.mode === 'edit' && (state.initialGrantedSubjects.length !== state.grantedSubjects.length - || state.initialGrantedSubjects.some(subject => !state.grantedSubjects.includes(subject)))); const cloudExists = connections.resource.values.some(isCloudConnection); if (cloudExists) { - infoItem = { - text: 'connections_connection_access_cloud_placeholder', - icon: '/icons/info_icon.svg', - }; + info = 'cloud_connections_access_placeholder'; } - if (unsaved) { - infoItem = { - text: 'connections_connection_access_save_reminder', - icon: '/icons/info_icon.svg', - }; + if (formState.mode === 'edit' && state.changed) { + info = 'ui_save_reminder'; } return styled(style)( - + {() => styled(style)( {!connections.resource.values.length ? ( @@ -114,25 +90,20 @@ export const GrantedConnections: TabContainerPanelComponent = ob ) : ( <> - {infoItem && ( - - - {translate(infoItem.text)} - - )} + {info && } - {state.editing && ( + {state.state.editing && ( )} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsList.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsList.tsx index 08a01486a9..1372c0c311 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsList.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsList.tsx @@ -95,9 +95,9 @@ export const GrantedConnectionList = observer(function GrantedConnectionL let tableInfoText: TLocalizationToken | null = null; if (!connections.length) { if (filterState.filterValue) { - tableInfoText = 'connections_connection_access_filter_no_result'; + tableInfoText = 'ui_search_no_result_placeholder'; } else { - tableInfoText = 'connections_connection_access_empty_table_placeholder'; + tableInfoText = 'ui_no_items_placeholder'; } } diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/useGrantedConnections.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/useGrantedConnections.tsx index 157ee8e92f..bebbe76cb1 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/useGrantedConnections.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/useGrantedConnections.tsx @@ -6,59 +6,67 @@ * you may not use this file except in compliance with the License. */ +import { action, computed, observable } from 'mobx'; + import { RoleInfo, RolesResource } from '@cloudbeaver/core-authentication'; -import { useObjectRef, useTabState } from '@cloudbeaver/core-blocks'; +import { useObservableRef, useTabState } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; import type { RoleFormMode } from '../IRoleFormProps'; import type { IGrantedConnectionsTabState } from './IGrantedConnectionsTabState'; -interface IConnectionAccessState { +interface State { state: IGrantedConnectionsTabState; + changed: boolean; + edit: () => void; revoke: (subjectIds: string[]) => void; grant: (subjectIds: string[]) => void; - edit: () => void; load: () => Promise; } -export function useGrantedConnections(role: RoleInfo, mode: RoleFormMode): IConnectionAccessState { +export function useGrantedConnections(role: RoleInfo, mode: RoleFormMode): Readonly { const resource = useService(RolesResource); const notificationService = useService(NotificationService); const state = useTabState(); - const edit = () => { - state.editing = !state.editing; - }; - - const revoke = (subjectIds: string[]): void => { - state.grantedSubjects = state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); - }; - - const grant = (subjectIds: string[]): void => { - state.grantedSubjects.push(...subjectIds); - }; - - const load = async () => { - if (state.loaded || state.loading) { - return; - } - - try { - state.loading = true; - - if (mode === 'edit') { - const grantInfo = await resource.getSubjectConnectionAccess(role.roleId); - state.grantedSubjects = grantInfo.map(subject => subject.connectionId); - state.initialGrantedSubjects = state.grantedSubjects.slice(); + return useObservableRef(() => ({ + get changed() { + return !isArraysEqual(this.state.initialGrantedSubjects, this.state.grantedSubjects); + }, + edit() { + this.state.editing = !this.state.editing; + }, + grant(subjectIds: string[]) { + this.state.grantedSubjects.push(...subjectIds); + }, + revoke(subjectIds: string[]) { + this.state.grantedSubjects = this.state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); + }, + async load() { + if (this.state.loaded || this.state.loading) { + return; } - state.loaded = true; - } catch (exception) { - notificationService.logException(exception, `Error getting granted connections for "${role.roleId}"`); - } - state.loading = false; - }; + try { + this.state.loading = true; - return useObjectRef({ state, revoke, grant, edit, load }); + if (this.mode === 'edit') { + const grantInfo = await this.resource.getSubjectConnectionAccess(this.role.roleId); + this.state.grantedSubjects = grantInfo.map(subject => subject.connectionId); + this.state.initialGrantedSubjects = this.state.grantedSubjects.slice(); + } + + this.state.loaded = true; + } catch (exception) { + this.notificationService.logException(exception, `Error getting granted connections for "${this.role.roleId}"`); + } finally { + this.state.loading = false; + } + }, + }), + { state: observable.ref, changed: computed, grant: action.bound, revoke: action.bound, edit: action.bound }, + { state, role, mode, resource, notificationService }, + ['load']); } diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUserList.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUserList.tsx index 5d8298722a..f53ef30b8d 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUserList.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUserList.tsx @@ -95,9 +95,9 @@ export const GrantedUserList = observer(function GrantedUserList({ let tableInfoText: TLocalizationToken | null = null; if (!users.length) { if (filterState.filterValue) { - tableInfoText = 'connections_connection_access_filter_no_result'; + tableInfoText = 'ui_search_no_result_placeholder'; } else { - tableInfoText = 'connections_connection_access_empty_table_placeholder'; + tableInfoText = 'ui_no_items_placeholder'; } } @@ -125,7 +125,7 @@ export const GrantedUserList = observer(function GrantedUserList({ id={user.userId} name={`${user.userId}${usersResource.isActiveUser(user.userId) ? ' (you)' : ''}`} icon='/icons/user.svg' - iconTooltip={translate('connections_connection_access_user_tooltip')} + iconTooltip={translate('authentication_user_icon_tooltip')} disabled={disabled} /> ))} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsers.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsers.tsx index 9404402b8a..bbd2f0c609 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsers.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsers.tsx @@ -14,10 +14,10 @@ import styled, { css } from 'reshadow'; import { UsersResource } from '@cloudbeaver/core-authentication'; import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, - IconOrImage, Loader, TabContainerPanelComponent, + InfoItem, Loader, TabContainerPanelComponent, TextPlaceholder, useMapResource, useTab, } from '@cloudbeaver/core-blocks'; -import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; +import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; import type { IRoleFormProps } from '../IRoleFormProps'; @@ -36,26 +36,11 @@ const styles = css` position: relative; overflow: auto !important; } - info-item { - display: flex; - align-items: center; - flex: 0 0 auto; - } - IconOrImage { - width: 24px; - height: 24px; - margin-right: 16px; - } Loader { z-index: 2; } `; -interface IInfoItem { - text: TLocalizationToken; - icon: string; -} - export const GrantedUsers: TabContainerPanelComponent = observer(function GrantedUsers({ tabId, state: formState, @@ -63,40 +48,27 @@ export const GrantedUsers: TabContainerPanelComponent = observer const style = useStyles(BASE_CONTAINERS_STYLES, styles); const translate = useTranslate(); - const { state, edit, grant, load, revoke } = useGrantedUsers(formState.config, formState.mode); - + const state = useGrantedUsers(formState.config, formState.mode); const { selected } = useTab(tabId); const users = useMapResource(UsersResource, selected ? UsersResource.keyAll : null); const grantedUsers = useMemo(() => computed(() => users.resource.values - .filter(user => state.grantedUsers.includes(user.userId)) - ), [state.grantedUsers, users.resource]); + .filter(user => state.state.grantedUsers.includes(user.userId)) + ), [state.state.grantedUsers, users.resource]); useEffect(() => { - if (selected && !state.loaded) { - load(); + if (selected && !state.state.loaded) { + state.load(); } - }, [selected, state.loaded, load]); + }, [selected, state.state.loaded]); if (!selected) { return null; } - let infoItem: IInfoItem | null = null; - - const unsaved = (formState.mode === 'edit' && (state.initialGrantedUsers.length !== state.grantedUsers.length - || state.initialGrantedUsers.some(subject => !state.grantedUsers.includes(subject)))); - - if (unsaved) { - infoItem = { - text: 'connections_connection_access_save_reminder', - icon: '/icons/info_icon.svg', - }; - } - return styled(style)( - + {() => styled(style)( {!users.resource.values.length ? ( @@ -105,25 +77,20 @@ export const GrantedUsers: TabContainerPanelComponent = observer ) : ( <> - {infoItem && ( - - - {translate(infoItem.text)} - - )} + {formState.mode === 'edit' && state.changed && } - {state.editing && ( + {state.state.editing && ( )} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/UserList.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/UserList.tsx index ebc83875b9..81b61380e5 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/UserList.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/UserList.tsx @@ -63,15 +63,15 @@ const styles = composes( interface Props { userList: AdminUserInfoFragment[]; grantedUsers: string[]; - onGrant: (subjectIds: string[]) => void; disabled: boolean; + onGrant: (subjectIds: string[]) => void; } export const UserList = observer(function UserList({ userList, grantedUsers, - onGrant, disabled, + onGrant, }) { const props = useObjectRef({ onGrant }); const style = useStyles(styles, BASE_CONTAINERS_STYLES); @@ -105,7 +105,7 @@ export const UserList = observer(function UserList({ {!users.length && filterState.filterValue && ( - {translate('connections_connection_access_filter_no_result')} + {translate('ui_search_no_result_placeholder')} )} @@ -115,7 +115,7 @@ export const UserList = observer(function UserList({ id={user.userId} name={`${user.userId}${usersResource.isActiveUser(user.userId) ? ' (you)' : ''}`} icon='/icons/user.svg' - iconTooltip={translate('connections_connection_access_user_tooltip')} + iconTooltip={translate('authentication_user_icon_tooltip')} disabled={disabled || !!grantedUsers.includes(user.userId)} /> ))} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/useGrantedUsers.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/useGrantedUsers.tsx index 4073e21f5c..f67773d41a 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/useGrantedUsers.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/useGrantedUsers.tsx @@ -6,60 +6,67 @@ * you may not use this file except in compliance with the License. */ +import { action, computed, observable } from 'mobx'; + import { RoleInfo, RolesResource } from '@cloudbeaver/core-authentication'; -import { useObjectRef, useTabState } from '@cloudbeaver/core-blocks'; +import { useObservableRef, useTabState } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; import type { RoleFormMode } from '../IRoleFormProps'; import type { IGrantedUsersTabState } from './IGrantedUsersTabState'; -interface IConnectionAccessState { +interface State { state: IGrantedUsersTabState; + changed: boolean; + edit: () => void; revoke: (subjectIds: string[]) => void; grant: (subjectIds: string[]) => void; - edit: () => void; load: () => Promise; } -export function useGrantedUsers(role: RoleInfo, mode: RoleFormMode): IConnectionAccessState { +export function useGrantedUsers(role: RoleInfo, mode: RoleFormMode): Readonly { const resource = useService(RolesResource); const notificationService = useService(NotificationService); const state = useTabState(); - const edit = () => { - state.editing = !state.editing; - }; - - const revoke = (subjectIds: string[]): void => { - state.grantedUsers = state.grantedUsers.filter(subject => !subjectIds.includes(subject)); - }; - - const grant = (subjectIds: string[]): void => { - state.grantedUsers.push(...subjectIds); - }; - - const load = async () => { - if (state.loaded || state.loading) { - return; - } - - try { - state.loading = true; - - if (mode === 'edit') { - const grantedUsers = await resource.loadGrantedUsers(role.roleId); - state.grantedUsers = grantedUsers; - - state.initialGrantedUsers = state.grantedUsers.slice(); + return useObservableRef(() => ({ + get changed() { + return !isArraysEqual(this.state.initialGrantedUsers, this.state.grantedUsers); + }, + edit() { + this.state.editing = !this.state.editing; + }, + revoke(subjectIds: string[]) { + this.state.grantedUsers = this.state.grantedUsers.filter(subject => !subjectIds.includes(subject)); + }, + grant(subjectIds: string[]) { + this.state.grantedUsers.push(...subjectIds); + }, + async load() { + if (this.state.loaded || this.state.loading) { + return; } - state.loaded = true; - } catch (exception) { - notificationService.logException(exception, "Can't load users info"); - } - state.loading = false; - }; + try { + this.state.loading = true; - return useObjectRef({ state, revoke, grant, edit, load }); + if (this.mode === 'edit') { + const grantedUsers = await this.resource.loadGrantedUsers(this.role.roleId); + this.state.grantedUsers = grantedUsers; + this.state.initialGrantedUsers = this.state.grantedUsers.slice(); + } + + this.state.loaded = true; + } catch (exception) { + this.notificationService.logException(exception, "Can't load users info"); + } finally { + this.state.loading = false; + } + }, + }), + { state: observable.ref, changed: computed, edit: action.bound, revoke: action.bound, grant: action.bound }, + { state, role, mode, resource, notificationService }, + ['load']); } diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministration.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministration.tsx index 5a8e2127d0..1cbcd1a52c 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministration.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministration.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css, use } from 'reshadow'; import { AdministrationItemContentComponent, ADMINISTRATION_TOOLS_PANEL_STYLES } from '@cloudbeaver/core-administration'; -import { BASE_CONTAINERS_STYLES, ToolsAction, Loader, ToolsPanel, useTable } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, ToolsAction, Loader, ToolsPanel } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; @@ -50,8 +50,7 @@ export const RolesAdministration: AdministrationItemContentComponent = observer( const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES, BASE_CONTAINERS_STYLES); const service = useService(CreateRoleService); - const tableState = useTable(); - const rolesTableState = useRolesTable(tableState); + const table = useRolesTable(); return styled(style)( <> @@ -60,7 +59,7 @@ export const RolesAdministration: AdministrationItemContentComponent = observer( title={translate('administration_roles_add_tooltip')} icon="add" viewBox="0 0 24 24" - disabled={!!sub || rolesTableState.processing} + disabled={!!sub || table.processing} onClick={service.create} > {translate('ui_add')} @@ -69,8 +68,8 @@ export const RolesAdministration: AdministrationItemContentComponent = observer( title={translate('administration_roles_refresh_tooltip')} icon="refresh" viewBox="0 0 24 24" - disabled={rolesTableState.processing} - onClick={rolesTableState.update} + disabled={table.processing} + onClick={table.update} > {translate('ui_refresh')} @@ -78,8 +77,8 @@ export const RolesAdministration: AdministrationItemContentComponent = observer( title={translate('administration_roles_delete_tooltip')} icon="trash" viewBox="0 0 24 24" - disabled={!tableState.itemsSelected || rolesTableState.processing} - onClick={rolesTableState.delete} + disabled={!table.tableState.itemsSelected || table.processing} + onClick={table.delete} > {translate('ui_delete')} @@ -93,11 +92,11 @@ export const RolesAdministration: AdministrationItemContentComponent = observer( )} - + diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/useRolesTable.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/useRolesTable.tsx index 8097292223..b509d5af99 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/useRolesTable.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/useRolesTable.tsx @@ -9,28 +9,28 @@ import { computed, observable } from 'mobx'; import { compareRoles, RoleInfo, RolesResource } from '@cloudbeaver/core-authentication'; -import { ITableState, useObservableRef } from '@cloudbeaver/core-blocks'; +import { TableState, useObservableRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { resourceKeyList } from '@cloudbeaver/core-sdk'; interface State { + tableState: TableState; processing: boolean; - tableState: ITableState; roles: RoleInfo[]; update: () => Promise; delete: () => Promise; } -export function useRolesTable(tableState: ITableState): State { +export function useRolesTable(): Readonly { const notificationService = useService(NotificationService); const dialogService = useService(CommonDialogService); const resource = useService(RolesResource); return useObservableRef(() => ({ + tableState: new TableState(), processing: false, - tableState, get roles() { return resource.values.slice().sort(compareRoles); }, @@ -85,5 +85,5 @@ export function useRolesTable(tableState: ITableState): State { }), { processing: observable.ref, roles: computed, - }, { tableState }, ['update', 'delete']); + }, false, ['update', 'delete']); } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/ConnectionAccess.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/ConnectionAccess.tsx index 09dfd51942..a098a80d9c 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/ConnectionAccess.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/ConnectionAccess.tsx @@ -96,7 +96,7 @@ export const ConnectionAccess: TabContainerPanelComponent = obse {cloudExists && ( - {translate('connections_connection_access_cloud_placeholder')} + {translate('cloud_connections_access_placeholder')} )} diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministration.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministration.tsx index 4adb221f4d..1e27a0f947 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministration.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministration.tsx @@ -34,7 +34,6 @@ const styles = composes( layout-grid { width: 100%; overflow: auto; - flex: 1; } layout-grid-inner { @@ -96,12 +95,14 @@ export const UsersAdministration: AdministrationItemContentComponent = observer( - - {controller.isProvidersLoading ? : ( - <> - {sub && service.user && ( + {controller.isProvidersLoading ? : ( + <> + {sub && service.user && ( + - )} + + )} + - - )} - + + + )} diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index e8a7193c4a..d35cb5a4cf 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -44,6 +44,9 @@ export default [ ['authentication_user_password_change_menu_title', 'Change local password'], ['authentication_user_password_change_success', 'Password was changed successfully'], ['authentication_user_password_change_dialog_submit', 'Change'], + ['authentication_user_icon_tooltip', 'User'], + ['authentication_role_icon_tooltip', 'Role'], + ['administration_configuration_wizard_configuration_admin', 'Administrator Credentials'], ['administration_configuration_wizard_configuration_admin_name', 'Login'], ['administration_configuration_wizard_configuration_admin_password', 'Password'], diff --git a/webapp/packages/plugin-authentication/src/locales/it.ts b/webapp/packages/plugin-authentication/src/locales/it.ts index fa3694793a..34818ef585 100644 --- a/webapp/packages/plugin-authentication/src/locales/it.ts +++ b/webapp/packages/plugin-authentication/src/locales/it.ts @@ -42,6 +42,9 @@ export default [ ['authentication_user_password_change_menu_title', 'Modifica la password locale'], ['authentication_user_password_change_success', 'Password modificata con successo'], ['authentication_user_password_change_dialog_submit', 'Modifica'], + ['authentication_user_icon_tooltip', 'Utente'], + ['authentication_role_icon_tooltip', 'Ruolo'], + ['administration_configuration_wizard_configuration_admin', 'Credenziali amministrative'], ['administration_configuration_wizard_configuration_admin_name', 'Login'], ['administration_configuration_wizard_configuration_admin_password', 'Password'], diff --git a/webapp/packages/plugin-authentication/src/locales/ru.ts b/webapp/packages/plugin-authentication/src/locales/ru.ts index 1f52a4a006..59e7905261 100644 --- a/webapp/packages/plugin-authentication/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication/src/locales/ru.ts @@ -42,6 +42,8 @@ export default [ ['authentication_user_password_change_menu_title', 'Изменить локальный пароль'], ['authentication_user_password_change_success', 'Пароль успешно изменен'], ['authentication_user_password_change_dialog_submit', 'Изменить'], + ['authentication_user_icon_tooltip', 'Пользователь'], + ['authentication_role_icon_tooltip', 'Роль'], ['administration_identity_providers_tab_title', 'Провайдеры идентификации'], ['administration_identity_providers_provider', 'Провайдер'], diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx index 0720a85025..3f8f0231e7 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; -import { ITableState, ToolsPanel } from '@cloudbeaver/core-blocks'; +import { TableState, ToolsPanel } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { ObjectPropertyTableFooterItem } from './ObjectPropertyTableFooterItem'; @@ -16,7 +16,7 @@ import { ObjectPropertyTableFooterService } from './ObjectPropertyTableFooterSer interface Props { nodeIds: string[]; - tableState: ITableState; + tableState: TableState; className?: string; } diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts index ca3837d7d7..413e9885cc 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -7,7 +7,7 @@ */ import { ENodeFeature, getNodeName, NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-app'; -import type { ITableState } from '@cloudbeaver/core-blocks'; +import type { TableState } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuContext, IContextMenuItem, IMenuItem, @@ -18,7 +18,7 @@ import { resourceKeyList } from '@cloudbeaver/core-sdk'; interface IObjectPropertyTableFooterContext { nodeIds: string[]; - tableState: ITableState; + tableState: TableState; } @injectable() @@ -93,7 +93,7 @@ export class ObjectPropertyTableFooterService { ); } - constructMenuWithContext(nodeIds: string[], tableState: ITableState): IMenuItem[] { + constructMenuWithContext(nodeIds: string[], tableState: TableState): IMenuItem[] { const context: IMenuContext = { menuId: this.objectPropertyTableFooterToken, contextType: ObjectPropertyTableFooterService.objectPropertyContextType,