mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #487 from dbeaver/fix/identity-providers
fix(plugin-authentication): CB-1241 cleanup
This commit is contained in:
@@ -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<Props>(function InfoItem({ info, icon = '/icons/info_icon.svg', className }) {
|
||||
const translate = useTranslate();
|
||||
return styled(styles)(
|
||||
<info-item className={className}>
|
||||
<IconOrImage icon={icon} />
|
||||
{translate(info)}
|
||||
</info-item>
|
||||
);
|
||||
});
|
||||
@@ -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<string, boolean>;
|
||||
expanded: Map<string, boolean>;
|
||||
|
||||
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<string, boolean> {
|
||||
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<string, boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, boolean>;
|
||||
expanded: Map<string, boolean>;
|
||||
readonly itemsSelected: boolean;
|
||||
readonly selectedList: string[];
|
||||
unselect: (key?: Key) => Map<string, boolean>;
|
||||
unexpand: (key?: Key) => Map<string, boolean>;
|
||||
}
|
||||
|
||||
export function useTable(): ITableState {
|
||||
return useObservableRef<ITableState>(() => ({
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
+26
-55
@@ -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<IConnectionFormProps> = 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<IConnectionFormProps>
|
||||
});
|
||||
|
||||
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)(
|
||||
<Loader state={[users, roles, state]}>
|
||||
<Loader state={[users, roles, state.state]}>
|
||||
{() => styled(style)(
|
||||
<ColoredContainer parent gap vertical>
|
||||
{!users.resource.values.length && !roles.resource.values.length ? (
|
||||
@@ -124,27 +100,22 @@ export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps>
|
||||
</Group>
|
||||
) : (
|
||||
<>
|
||||
{infoItem && (
|
||||
<info-item>
|
||||
<IconOrImage icon={infoItem.icon} />
|
||||
{translate(infoItem.text)}
|
||||
</info-item>
|
||||
)}
|
||||
{info && <InfoItem info={info} />}
|
||||
<Container gap overflow>
|
||||
<ConnectionAccessGrantedList
|
||||
grantedUsers={grantedUsers.get()}
|
||||
grantedRoles={grantedRoles.get()}
|
||||
disabled={disabled}
|
||||
onEdit={edit}
|
||||
onRevoke={revoke}
|
||||
onEdit={state.edit}
|
||||
onRevoke={state.revoke}
|
||||
/>
|
||||
{state.editing && (
|
||||
{state.state.editing && (
|
||||
<ConnectionAccessList
|
||||
userList={users.resource.values}
|
||||
roleList={roles.resource.values}
|
||||
grantedSubjects={state.grantedSubjects}
|
||||
grantedSubjects={state.state.grantedSubjects}
|
||||
disabled={disabled}
|
||||
onGrant={grant}
|
||||
onGrant={state.grant}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
+4
-4
@@ -98,9 +98,9 @@ export const ConnectionAccessGrantedList = observer<Props>(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<Props>(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<Props>(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}
|
||||
/>
|
||||
))}
|
||||
|
||||
+3
-3
@@ -108,7 +108,7 @@ export const ConnectionAccessList = observer<Props>(function ConnectionAccessLis
|
||||
{!roles.get().length && !users.get().length && filterState.filterValue && (
|
||||
<TableItem item='tableInfo' selectDisabled>
|
||||
<TableColumnValue colSpan={5}>
|
||||
{translate('connections_connection_access_filter_no_result')}
|
||||
{translate('ui_search_no_result_placeholder')}
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
)}
|
||||
@@ -119,7 +119,7 @@ export const ConnectionAccessList = observer<Props>(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<Props>(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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
+44
-36
@@ -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<void>;
|
||||
}
|
||||
|
||||
export function useConnectionAccessState(connection: DatabaseConnectionFragment | undefined): IConnectionAccessState {
|
||||
const connectionsResource = useService(ConnectionsResource);
|
||||
export function useConnectionAccessState(connection: DatabaseConnectionFragment | undefined): Readonly<State> {
|
||||
const resource = useService(ConnectionsResource);
|
||||
const notificationService = useService(NotificationService);
|
||||
const state = useTabState<IConnectionAccessTabState>();
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
@@ -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".'],
|
||||
];
|
||||
|
||||
@@ -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".'],
|
||||
];
|
||||
|
||||
@@ -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".'],
|
||||
];
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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.'],
|
||||
|
||||
@@ -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', 'Ваша сессия скоро закончится'],
|
||||
|
||||
+11
-12
@@ -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')}
|
||||
</ToolsAction>
|
||||
@@ -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')}
|
||||
</ToolsAction>
|
||||
@@ -94,11 +93,11 @@ export const AuthConfigurationsAdministration: AdministrationItemContentComponen
|
||||
)}
|
||||
<layout-grid-cell {...use({ span: 12 })}>
|
||||
<AuthConfigurationsTable
|
||||
configurations={configurationsTableState.configurations}
|
||||
selectedItems={tableState.selected}
|
||||
expandedItems={tableState.expanded}
|
||||
configurations={table.configurations}
|
||||
selectedItems={table.tableState.selected}
|
||||
expandedItems={table.tableState.expanded}
|
||||
/>
|
||||
<Loader loading={configurationsTableState.processing} overlay />
|
||||
<Loader loading={table.processing} overlay />
|
||||
</layout-grid-cell>
|
||||
</layout-grid-inner>
|
||||
</layout-grid>
|
||||
|
||||
+5
-5
@@ -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<void>;
|
||||
delete: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useConfigurationsTable(tableState: ITableState): State {
|
||||
export function useConfigurationsTable(): Readonly<State> {
|
||||
const notificationService = useService(NotificationService);
|
||||
const dialogService = useService(CommonDialogService);
|
||||
const resource = useService(AuthConfigurationsResource);
|
||||
|
||||
return useObservableRef<State>(() => ({
|
||||
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']);
|
||||
}
|
||||
|
||||
-4
@@ -153,8 +153,6 @@ export const AuthConfigurationOptions: TabContainerPanelComponent<IAuthConfigura
|
||||
title={state.config.signInLink}
|
||||
disabled={state.disabled}
|
||||
readOnly
|
||||
tiny
|
||||
fill
|
||||
>
|
||||
Sign in
|
||||
</InputFieldNew>
|
||||
@@ -164,8 +162,6 @@ export const AuthConfigurationOptions: TabContainerPanelComponent<IAuthConfigura
|
||||
title={state.config.signOutLink}
|
||||
disabled={state.disabled}
|
||||
readOnly
|
||||
tiny
|
||||
fill
|
||||
>
|
||||
Sign out
|
||||
</InputFieldNew>
|
||||
|
||||
+3
-3
@@ -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<Props>(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<Props>(function ConnectionList({
|
||||
{!connections.length && filterState.filterValue && (
|
||||
<TableItem item='tableInfo' selectDisabled>
|
||||
<TableColumnValue colSpan={5}>
|
||||
{translate('connections_connection_access_filter_no_result')}
|
||||
{translate('ui_search_no_result_placeholder')}
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
)}
|
||||
|
||||
+18
-47
@@ -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<IRoleFormProps> = observer(function GrantedConnections({
|
||||
tabId,
|
||||
state: formState,
|
||||
@@ -63,49 +48,40 @@ export const GrantedConnections: TabContainerPanelComponent<IRoleFormProps> = 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)(
|
||||
<Loader state={[connections, dbDriverResource, state]}>
|
||||
<Loader state={[connections, dbDriverResource, state.state]}>
|
||||
{() => styled(style)(
|
||||
<ColoredContainer parent gap vertical>
|
||||
{!connections.resource.values.length ? (
|
||||
@@ -114,25 +90,20 @@ export const GrantedConnections: TabContainerPanelComponent<IRoleFormProps> = ob
|
||||
</Group>
|
||||
) : (
|
||||
<>
|
||||
{infoItem && (
|
||||
<info-item>
|
||||
<IconOrImage icon={infoItem.icon} />
|
||||
{translate(infoItem.text)}
|
||||
</info-item>
|
||||
)}
|
||||
{info && <InfoItem info={info} />}
|
||||
<Container gap overflow>
|
||||
<GrantedConnectionList
|
||||
grantedConnections={grantedConnections.get()}
|
||||
disabled={formState.disabled}
|
||||
onEdit={edit}
|
||||
onRevoke={revoke}
|
||||
onEdit={state.edit}
|
||||
onRevoke={state.revoke}
|
||||
/>
|
||||
{state.editing && (
|
||||
{state.state.editing && (
|
||||
<ConnectionList
|
||||
connectionList={connections.resource.values}
|
||||
grantedSubjects={state.grantedSubjects}
|
||||
grantedSubjects={state.state.grantedSubjects}
|
||||
disabled={formState.disabled}
|
||||
onGrant={grant}
|
||||
onGrant={state.grant}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
+2
-2
@@ -95,9 +95,9 @@ export const GrantedConnectionList = observer<Props>(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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
-35
@@ -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<void>;
|
||||
}
|
||||
|
||||
export function useGrantedConnections(role: RoleInfo, mode: RoleFormMode): IConnectionAccessState {
|
||||
export function useGrantedConnections(role: RoleInfo, mode: RoleFormMode): Readonly<State> {
|
||||
const resource = useService(RolesResource);
|
||||
const notificationService = useService(NotificationService);
|
||||
const state = useTabState<IGrantedConnectionsTabState>();
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
+3
-3
@@ -95,9 +95,9 @@ export const GrantedUserList = observer<Props>(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<Props>(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}
|
||||
/>
|
||||
))}
|
||||
|
||||
+15
-48
@@ -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<IRoleFormProps> = observer(function GrantedUsers({
|
||||
tabId,
|
||||
state: formState,
|
||||
@@ -63,40 +48,27 @@ export const GrantedUsers: TabContainerPanelComponent<IRoleFormProps> = 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)(
|
||||
<Loader state={[users, state]}>
|
||||
<Loader state={[users, state.state]}>
|
||||
{() => styled(style)(
|
||||
<ColoredContainer parent gap vertical>
|
||||
{!users.resource.values.length ? (
|
||||
@@ -105,25 +77,20 @@ export const GrantedUsers: TabContainerPanelComponent<IRoleFormProps> = observer
|
||||
</Group>
|
||||
) : (
|
||||
<>
|
||||
{infoItem && (
|
||||
<info-item>
|
||||
<IconOrImage icon={infoItem.icon} />
|
||||
{translate(infoItem.text)}
|
||||
</info-item>
|
||||
)}
|
||||
{formState.mode === 'edit' && state.changed && <InfoItem info='ui_save_reminder' />}
|
||||
<Container gap overflow>
|
||||
<GrantedUserList
|
||||
grantedUsers={grantedUsers.get()}
|
||||
disabled={formState.disabled}
|
||||
onEdit={edit}
|
||||
onRevoke={revoke}
|
||||
onEdit={state.edit}
|
||||
onRevoke={state.revoke}
|
||||
/>
|
||||
{state.editing && (
|
||||
{state.state.editing && (
|
||||
<UserList
|
||||
userList={users.resource.values}
|
||||
grantedUsers={state.grantedUsers}
|
||||
grantedUsers={state.state.grantedUsers}
|
||||
disabled={formState.disabled}
|
||||
onGrant={grant}
|
||||
onGrant={state.grant}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
+4
-4
@@ -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<Props>(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<Props>(function UserList({
|
||||
{!users.length && filterState.filterValue && (
|
||||
<TableItem item='tableInfo' selectDisabled>
|
||||
<TableColumnValue colSpan={5}>
|
||||
{translate('connections_connection_access_filter_no_result')}
|
||||
{translate('ui_search_no_result_placeholder')}
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
)}
|
||||
@@ -115,7 +115,7 @@ export const UserList = observer<Props>(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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
+43
-36
@@ -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<void>;
|
||||
}
|
||||
|
||||
export function useGrantedUsers(role: RoleInfo, mode: RoleFormMode): IConnectionAccessState {
|
||||
export function useGrantedUsers(role: RoleInfo, mode: RoleFormMode): Readonly<State> {
|
||||
const resource = useService(RolesResource);
|
||||
const notificationService = useService(NotificationService);
|
||||
const state = useTabState<IGrantedUsersTabState>();
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
+11
-12
@@ -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')}
|
||||
</ToolsAction>
|
||||
@@ -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')}
|
||||
</ToolsAction>
|
||||
@@ -93,11 +92,11 @@ export const RolesAdministration: AdministrationItemContentComponent = observer(
|
||||
)}
|
||||
<layout-grid-cell {...use({ span: 12 })}>
|
||||
<RolesTable
|
||||
roles={rolesTableState.roles}
|
||||
selectedItems={tableState.selected}
|
||||
expandedItems={tableState.expanded}
|
||||
roles={table.roles}
|
||||
selectedItems={table.tableState.selected}
|
||||
expandedItems={table.tableState.expanded}
|
||||
/>
|
||||
<Loader loading={false} overlay />
|
||||
<Loader loading={table.processing} overlay />
|
||||
</layout-grid-cell>
|
||||
</layout-grid-inner>
|
||||
</layout-grid>
|
||||
|
||||
+5
-5
@@ -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<void>;
|
||||
delete: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useRolesTable(tableState: ITableState): State {
|
||||
export function useRolesTable(): Readonly<State> {
|
||||
const notificationService = useService(NotificationService);
|
||||
const dialogService = useService(CommonDialogService);
|
||||
const resource = useService(RolesResource);
|
||||
|
||||
return useObservableRef<State>(() => ({
|
||||
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']);
|
||||
}
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ export const ConnectionAccess: TabContainerPanelComponent<IUserFormProps> = obse
|
||||
{cloudExists && (
|
||||
<TableItem item='cloudInfo' selectDisabled>
|
||||
<TableColumnValue colSpan={5}>
|
||||
{translate('connections_connection_access_cloud_placeholder')}
|
||||
{translate('cloud_connections_access_placeholder')}
|
||||
</TableColumnValue>
|
||||
</TableItem>
|
||||
)}
|
||||
|
||||
+10
-9
@@ -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(
|
||||
</ToolsPanel>
|
||||
<layout-grid>
|
||||
<layout-grid-inner>
|
||||
<layout-grid-cell {...use({ span: 12 })}>
|
||||
{controller.isProvidersLoading ? <Loader /> : (
|
||||
<>
|
||||
{sub && service.user && (
|
||||
{controller.isProvidersLoading ? <Loader /> : (
|
||||
<>
|
||||
{sub && service.user && (
|
||||
<layout-grid-cell {...use({ span: 12 })}>
|
||||
<CreateUser user={service.user} onCancel={service.cancelCreate} />
|
||||
)}
|
||||
</layout-grid-cell>
|
||||
)}
|
||||
<layout-grid-cell {...use({ span: 12 })}>
|
||||
<UsersTable
|
||||
users={controller.users}
|
||||
selectedItems={controller.selectedItems}
|
||||
@@ -109,9 +110,9 @@ export const UsersAdministration: AdministrationItemContentComponent = observer(
|
||||
selectable={controller.isLocalProviderAvailable}
|
||||
/>
|
||||
<Loader loading={controller.isLoading} overlay />
|
||||
</>
|
||||
)}
|
||||
</layout-grid-cell>
|
||||
</layout-grid-cell>
|
||||
</>
|
||||
)}
|
||||
</layout-grid-inner>
|
||||
</layout-grid>
|
||||
</>
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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', 'Провайдер'],
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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<IObjectPropertyTableFooterContext> = {
|
||||
menuId: this.objectPropertyTableFooterToken,
|
||||
contextType: ObjectPropertyTableFooterService.objectPropertyContextType,
|
||||
|
||||
Reference in New Issue
Block a user