diff --git a/webapp/packages/core-authentication/src/RolesResource.ts b/webapp/packages/core-authentication/src/RolesResource.ts index bde79805e4..773452078c 100644 --- a/webapp/packages/core-authentication/src/RolesResource.ts +++ b/webapp/packages/core-authentication/src/RolesResource.ts @@ -9,14 +9,25 @@ import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService, - AdminRoleInfo, CachedMapResource, - ResourceKey + ResourceKey, + resourceKeyList, + ResourceKeyList, + ResourceKeyUtils, + AdminRoleInfoFragment, + AdminConnectionGrantInfo } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; +const NEW_ROLE_SYMBOL = Symbol('new-role'); + +export type RoleInfo = AdminRoleInfoFragment; +type NewRole = RoleInfo & { [NEW_ROLE_SYMBOL]: boolean; timestamp: number }; + @injectable() -export class RolesResource extends CachedMapResource { +export class RolesResource extends CachedMapResource { + static keyAll = resourceKeyList(['all'], 'all'); + private loadedKeyMetadata: MetadataMap; constructor(private graphQLService: GraphQLService) { super(); @@ -31,30 +42,113 @@ export class RolesResource extends CachedMapResource { return this.data.has(id); } - async loadAll() { - await this.load('all'); + async loadAll(): Promise> { + await this.load(RolesResource.keyAll); return this.data; } - async refreshAll() { - await this.refresh('all'); + async refreshAll(): Promise> { + await this.refresh(RolesResource.keyAll); return this.data; } - protected async loader(key: ResourceKey): Promise> { - const roleId = key === 'all' ? undefined : key as string; + async createRole(roleInfo: RoleInfo): Promise { + const response = await this.graphQLService.sdk.createRole(roleInfo); - const { roles } = await this.graphQLService.sdk.getRolesList({ roleId }); + const newRole: NewRole = { + ...response.role, + [NEW_ROLE_SYMBOL]: true, + timestamp: Date.now(), + }; - if (key === 'all') { - this.data.clear(); - this.loadedKeyMetadata.set('all', true); - } + this.updateRoles(newRole); - for (const role of roles) { - this.set(role.roleId, role as AdminRoleInfo); - } + return this.get(roleInfo.roleId)!; + } + + async updateRole(roleInfo: RoleInfo): Promise { + const { role } = await this.graphQLService.sdk.updateRole(roleInfo); + + this.updateRoles(role); + + return this.get(roleInfo.roleId)!; + } + + async deleteRole(key: ResourceKey): Promise> { + await ResourceKeyUtils.forEachAsync(key, async key => { + await this.graphQLService.sdk.deleteRole({ + roleId: key, + }); + this.delete(key); + }); return this.data; } + + async loadGrantedUsers(roleId: string): Promise { + const { role } = await this.graphQLService.sdk.getRoleGrantedUsers({ roleId }); + return role[0].grantedUsers; + } + + async getSubjectConnectionAccess(subjectId: string): Promise { + const { grantInfo } = await this.graphQLService.sdk.getSubjectConnectionAccess({ subjectId }); + return grantInfo; + } + + protected async loader(key: ResourceKey): Promise> { + const all = ResourceKeyUtils.hasMark(key, RolesResource.keyAll.mark); + + await ResourceKeyUtils.forEachAsync(all ? RolesResource.keyAll : key, async key => { + const { roles } = await this.graphQLService.sdk.getRolesList({ + roleId: !all ? key : undefined, + }); + + if (all) { + this.data.clear(); + } + + this.updateRoles(...roles); + + if (all) { + this.loadedKeyMetadata.set(RolesResource.keyAll.list[0], true); + } + }); + + return this.data; + } + + cleanNewFlags(): void { + for (const role of this.data.values()) { + (role as NewRole)[NEW_ROLE_SYMBOL] = false; + } + } + + private updateRoles(...roles: RoleInfo[]): ResourceKeyList { + const key = resourceKeyList(roles.map(role => role.roleId)); + + const oldRoles = this.get(key); + this.set(key, oldRoles.map((role, i) => ({ ...role, ...roles[i] }))); + + return key; + } +} + +function isNewRole(role: RoleInfo | NewRole): role is NewRole { + return (role as NewRole)[NEW_ROLE_SYMBOL]; +} + +export function compareRoles(a: RoleInfo, b: RoleInfo): number { + if (isNewRole(a) && isNewRole(b)) { + return b.timestamp - a.timestamp; + } + + if (isNewRole(b)) { + return 1; + } + + if (isNewRole(a)) { + return -1; + } + + return a.roleId.localeCompare(b.roleId); } diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index 54fd396ba2..c0ee044ff8 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -182,6 +182,10 @@ export class UsersResource extends CachedMapResource, includes: string[] | undefined): Promise> { await ResourceKeyUtils.forEachAsync(key, async key => { const { users } = await this.graphQLService.sdk.getUsersList({ @@ -206,10 +210,6 @@ export class UsersResource extends CachedMapResource>(functio const [toggled, setToggled] = useState(!toggleMode); const filter = useCallback((value: string | number, name?: string) => { - value = String(value).trim(); + value = String(value); if (state && name) { state[name] = value; diff --git a/webapp/packages/core-blocks/src/Table/TableColumnValue.tsx b/webapp/packages/core-blocks/src/Table/TableColumnValue.tsx index 61ff46b9ec..e50f8959ea 100644 --- a/webapp/packages/core-blocks/src/Table/TableColumnValue.tsx +++ b/webapp/packages/core-blocks/src/Table/TableColumnValue.tsx @@ -22,6 +22,7 @@ type Props = { align?: 'left' | 'center' | 'right' | 'justify' | 'char'; className?: string; centerContent?: boolean; + ellipsis?: boolean; flex?: boolean; expand?: boolean; onClick?: () => void; @@ -32,6 +33,7 @@ export const TableColumnValue = observer(function TableColumnValue({ align, children, centerContent, + ellipsis, flex, expand, className, @@ -74,7 +76,7 @@ export const TableColumnValue = observer(function TableColumnValue({ void; onEdit: () => void; diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx index 0e1c42e59e..6c240440bd 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx @@ -11,6 +11,7 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useMemo, useState } from 'react'; import styled, { css } from 'reshadow'; +import type { RoleInfo } from '@cloudbeaver/core-authentication'; import { Table, TableBody, @@ -22,7 +23,7 @@ import { useObjectRef, } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; -import type { AdminRoleInfo, AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import { ConnectionAccessTableHeader, IFilterState } from './ConnectionAccessTableHeader/ConnectionAccessTableHeader'; @@ -59,7 +60,7 @@ const styles = composes( interface Props { userList: AdminUserInfoFragment[]; - roleList: AdminRoleInfo[]; + roleList: RoleInfo[]; grantedSubjects: string[]; onGrant: (subjectIds: string[]) => void; disabled: boolean; diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/getFilteredSubjects.ts b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/getFilteredSubjects.ts index 5fd9d160bc..dd8eb77809 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/getFilteredSubjects.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/getFilteredSubjects.ts @@ -6,7 +6,8 @@ * you may not use this file except in compliance with the License. */ -import type { AdminRoleInfo, AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; +import type { RoleInfo } from '@cloudbeaver/core-authentication'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; /** * @param {AdminUserInfoFragment[]} users @@ -22,7 +23,7 @@ export function getFilteredUsers(users: AdminUserInfoFragment[], filter: string) * @param {AdminRoleInfo[]} roles * @param {string} filter */ -export function getFilteredRoles(roles: AdminRoleInfo[], filter: string): AdminRoleInfo[] { +export function getFilteredRoles(roles: RoleInfo[], filter: string): RoleInfo[] { return roles .filter(role => role.roleName?.toLowerCase().includes(filter.toLowerCase()) && role.roleId !== 'admin') .sort((a, b) => (a.roleName ?? '').localeCompare(b.roleName ?? '')); diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index b9b9b22d20..65076d62c8 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -34,6 +34,10 @@ export const defaultENLocale = [ ['ui_clipboard_access_denied_message', 'You need to give access to clipboard to use some of the app features'], ['ui_reveal_password', 'Show or hide password'], ['ui_page_not_found', 'Page is not found'], + ['ui_revoke', 'Revoke'], + ['ui_grant', 'Grant'], + ['ui_edit', 'Edit'], + ['root_permission_denied', 'You have no permissions'], ['app_root_session_expire_warning_title', 'Session is about to expire'], ['app_root_session_expire_warning_message', 'Your session will expire in less than 5 minutes. To keep working, close this popup.'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 3300e8f1c7..f99f218444 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -32,6 +32,10 @@ export const defaultRULocale = [ ['ui_clipboard_access_denied_message', 'Чтобы использовать некоторые функции приложения, необходим доступ к буферу обмена'], ['ui_reveal_password', 'Показать или скрыть пароль'], ['ui_page_not_found', 'Страница не найдена'], + ['ui_revoke', 'Запретить'], + ['ui_grant', 'Разрешить'], + ['ui_edit', 'Редактировать'], + ['root_permission_denied', 'Отказано в доступе'], ['app_root_session_expire_warning_title', 'Ваша сессия скоро закончится'], ['app_root_session_expire_warning_message', 'Ваша сессия закончится меньше чем через 5 минут. Чтобы продолжить работать, закройте этот диалог.'], diff --git a/webapp/packages/core-sdk/src/queries/authentication/Roles/createRole.gql b/webapp/packages/core-sdk/src/queries/authentication/Roles/createRole.gql new file mode 100644 index 0000000000..72fc091327 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/Roles/createRole.gql @@ -0,0 +1,5 @@ +query createRole($roleId: ID!, $roleName: String, $description: String ) { + role: createRole(roleId: $roleId, roleName: $roleName, description: $description) { + ...AdminRoleInfo + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/authentication/Roles/deleteRole.gql b/webapp/packages/core-sdk/src/queries/authentication/Roles/deleteRole.gql new file mode 100644 index 0000000000..9ccaa6c1e1 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/Roles/deleteRole.gql @@ -0,0 +1,3 @@ +query deleteRole($roleId: ID!) { + deleteRole(roleId: $roleId) +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/authentication/Roles/getRoleGrantedUsers.gql b/webapp/packages/core-sdk/src/queries/authentication/Roles/getRoleGrantedUsers.gql new file mode 100644 index 0000000000..398b2a64de --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/Roles/getRoleGrantedUsers.gql @@ -0,0 +1,5 @@ +query getRoleGrantedUsers($roleId: ID!) { + role: listRoles(roleId: $roleId) { + grantedUsers + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/authentication/users/getRolesList.gql b/webapp/packages/core-sdk/src/queries/authentication/Roles/getRolesList.gql similarity index 56% rename from webapp/packages/core-sdk/src/queries/authentication/users/getRolesList.gql rename to webapp/packages/core-sdk/src/queries/authentication/Roles/getRolesList.gql index 2d49828a8b..8768413470 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/users/getRolesList.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/Roles/getRolesList.gql @@ -1,8 +1,5 @@ query getRolesList($roleId: ID) { roles: listRoles(roleId: $roleId) { - roleId - roleName - description - #rolePermissions + ...AdminRoleInfo } } \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/authentication/Roles/updateRole.gql b/webapp/packages/core-sdk/src/queries/authentication/Roles/updateRole.gql new file mode 100644 index 0000000000..a35a5ba5f7 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/Roles/updateRole.gql @@ -0,0 +1,5 @@ +query updateRole($roleId: ID!, $roleName: String, $description: String ) { + role: updateRole(roleId: $roleId, roleName: $roleName, description: $description) { + ...AdminRoleInfo + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/connections/administration/getSubjectConnectionAccess.gql b/webapp/packages/core-sdk/src/queries/connections/administration/getSubjectConnectionAccess.gql new file mode 100644 index 0000000000..321aec3a52 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/connections/administration/getSubjectConnectionAccess.gql @@ -0,0 +1,7 @@ +query getSubjectConnectionAccess($subjectId: ID) { + grantInfo: getSubjectConnectionAccess(subjectId: $subjectId) { + connectionId, + subjectId, + subjectType, + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/connections/administration/setSubjectConnectionAccess.gql b/webapp/packages/core-sdk/src/queries/connections/administration/setSubjectConnectionAccess.gql new file mode 100644 index 0000000000..9a64d6b677 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/connections/administration/setSubjectConnectionAccess.gql @@ -0,0 +1,3 @@ +query setSubjectConnectionAccess($subjectId: ID!, $connections: [ID!]!) { + setSubjectConnectionAccess(subjectId: $subjectId, connections: $connections) +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/fragments/AdminRoleInfo.gql b/webapp/packages/core-sdk/src/queries/fragments/AdminRoleInfo.gql new file mode 100644 index 0000000000..96d091863e --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/fragments/AdminRoleInfo.gql @@ -0,0 +1,5 @@ +fragment AdminRoleInfo on AdminRoleInfo { + roleId + roleName + description +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 19e1b1bdde..9ecb2c3b22 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -52,6 +52,8 @@ export interface AdminRoleInfo { roleId: Scalars['ID']; roleName?: Maybe; description?: Maybe; + grantedUsers: Array; + grantedConnections: AdminConnectionGrantInfo[]; rolePermissions: Array>; } @@ -1110,6 +1112,40 @@ export type AsyncTaskCancelMutationVariables = Exact<{ export interface AsyncTaskCancelMutation { result: Mutation['asyncTaskCancel'] } +export type CreateRoleQueryVariables = Exact<{ + roleId: Scalars['ID']; + roleName?: Maybe; + description?: Maybe; +}>; + +export interface CreateRoleQuery { role: AdminRoleInfoFragment } + +export type DeleteRoleQueryVariables = Exact<{ + roleId: Scalars['ID']; +}>; + +export type DeleteRoleQuery = Pick; + +export type GetRoleGrantedUsersQueryVariables = Exact<{ + roleId: Scalars['ID']; +}>; + +export interface GetRoleGrantedUsersQuery { role: Array>> } + +export type GetRolesListQueryVariables = Exact<{ + roleId?: Maybe; +}>; + +export interface GetRolesListQuery { roles: Array> } + +export type UpdateRoleQueryVariables = Exact<{ + roleId: Scalars['ID']; + roleName?: Maybe; + description?: Maybe; +}>; + +export interface UpdateRoleQuery { role: AdminRoleInfoFragment } + export type AuthChangeLocalPasswordQueryVariables = Exact<{ oldPassword: Scalars['String']; newPassword: Scalars['String']; @@ -1199,12 +1235,6 @@ export type GetPermissionsListQueryVariables = Exact<{ export interface GetPermissionsListQuery { permissions: Array>> } -export type GetRolesListQueryVariables = Exact<{ - roleId?: Maybe; -}>; - -export interface GetRolesListQuery { roles: Array>> } - export type GetUserGrantedConnectionsQueryVariables = Exact<{ userId?: Maybe; }>; @@ -1290,6 +1320,12 @@ export type GetConnectionsQueryVariables = Exact<{ export interface GetConnectionsQuery { connections: DatabaseConnectionFragment[] } +export type GetSubjectConnectionAccessQueryVariables = Exact<{ + subjectId?: Maybe; +}>; + +export interface GetSubjectConnectionAccessQuery { grantInfo: Array> } + export type SearchDatabasesQueryVariables = Exact<{ hosts: Array | Scalars['String']; }>; @@ -1303,6 +1339,13 @@ export type SetConnectionAccessQueryVariables = Exact<{ export type SetConnectionAccessQuery = Pick; +export type SetSubjectConnectionAccessQueryVariables = Exact<{ + subjectId: Scalars['ID']; + connections: Array | Scalars['ID']; +}>; + +export type SetSubjectConnectionAccessQuery = Pick; + export type UpdateConnectionConfigurationQueryVariables = Exact<{ id: Scalars['ID']; config: ConnectionConfig; @@ -1534,6 +1577,8 @@ export type NavGetStructContainersQueryVariables = Exact<{ export interface NavGetStructContainersQuery { navGetStructContainers: { catalogList: Array>; schemaList: Array> } } +export type AdminRoleInfoFragment = Pick; + export type AdminUserInfoFragment = ( Pick & { origins: ObjectOriginInfoFragment[] } @@ -1835,6 +1880,13 @@ export type SqlResultCloseMutationVariables = Exact<{ export interface SqlResultCloseMutation { result: Mutation['sqlResultClose'] } +export const AdminRoleInfoFragmentDoc = ` + fragment AdminRoleInfo on AdminRoleInfo { + roleId + roleName + description +} + `; export const ObjectOriginInfoFragmentDoc = ` fragment ObjectOriginInfo on ObjectOrigin { type @@ -2060,6 +2112,47 @@ export const AsyncTaskCancelDocument = ` result: asyncTaskCancel(id: $taskId) } `; +export const CreateRoleDocument = ` + query createRole($roleId: ID!, $roleName: String, $description: String) { + role: createRole( + roleId: $roleId + roleName: $roleName + description: $description + ) { + ...AdminRoleInfo + } +} + ${AdminRoleInfoFragmentDoc}`; +export const DeleteRoleDocument = ` + query deleteRole($roleId: ID!) { + deleteRole(roleId: $roleId) +} + `; +export const GetRoleGrantedUsersDocument = ` + query getRoleGrantedUsers($roleId: ID!) { + role: listRoles(roleId: $roleId) { + grantedUsers + } +} + `; +export const GetRolesListDocument = ` + query getRolesList($roleId: ID) { + roles: listRoles(roleId: $roleId) { + ...AdminRoleInfo + } +} + ${AdminRoleInfoFragmentDoc}`; +export const UpdateRoleDocument = ` + query updateRole($roleId: ID!, $roleName: String, $description: String) { + role: updateRole( + roleId: $roleId + roleName: $roleName + description: $description + ) { + ...AdminRoleInfo + } +} + ${AdminRoleInfoFragmentDoc}`; export const AuthChangeLocalPasswordDocument = ` query authChangeLocalPassword($oldPassword: String!, $newPassword: String!) { authChangeLocalPassword(oldPassword: $oldPassword, newPassword: $newPassword) @@ -2193,15 +2286,6 @@ export const GetPermissionsListDocument = ` } } `; -export const GetRolesListDocument = ` - query getRolesList($roleId: ID) { - roles: listRoles(roleId: $roleId) { - roleId - roleName - description - } -} - `; export const GetUserGrantedConnectionsDocument = ` query getUserGrantedConnections($userId: ID) { grantedConnections: getSubjectConnectionAccess(subjectId: $userId) { @@ -2280,6 +2364,15 @@ export const GetConnectionsDocument = ` } } ${DatabaseConnectionFragmentDoc}`; +export const GetSubjectConnectionAccessDocument = ` + query getSubjectConnectionAccess($subjectId: ID) { + grantInfo: getSubjectConnectionAccess(subjectId: $subjectId) { + connectionId + subjectId + subjectType + } +} + `; export const SearchDatabasesDocument = ` query searchDatabases($hosts: [String!]!) { databases: searchConnections(hostNames: $hosts) { @@ -2296,6 +2389,11 @@ export const SetConnectionAccessDocument = ` setConnectionSubjectAccess(connectionId: $connectionId, subjects: $subjects) } `; +export const SetSubjectConnectionAccessDocument = ` + query setSubjectConnectionAccess($subjectId: ID!, $connections: [ID!]!) { + setSubjectConnectionAccess(subjectId: $subjectId, connections: $connections) +} + `; export const UpdateConnectionConfigurationDocument = ` query updateConnectionConfiguration($id: ID!, $config: ConnectionConfig!, $includeOrigin: Boolean!, $customIncludeOriginDetails: Boolean!, $includeAuthProperties: Boolean!, $customIncludeNetworkHandlerCredentials: Boolean!) { connection: updateConnectionConfiguration(id: $id, config: $config) { @@ -2962,6 +3060,21 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = asyncTaskCancel(variables: AsyncTaskCancelMutationVariables): Promise { return withWrapper(() => client.request(AsyncTaskCancelDocument, variables)); }, + createRole(variables: CreateRoleQueryVariables): Promise { + return withWrapper(() => client.request(CreateRoleDocument, variables)); + }, + deleteRole(variables: DeleteRoleQueryVariables): Promise { + return withWrapper(() => client.request(DeleteRoleDocument, variables)); + }, + getRoleGrantedUsers(variables: GetRoleGrantedUsersQueryVariables): Promise { + return withWrapper(() => client.request(GetRoleGrantedUsersDocument, variables)); + }, + getRolesList(variables?: GetRolesListQueryVariables): Promise { + return withWrapper(() => client.request(GetRolesListDocument, variables)); + }, + updateRole(variables: UpdateRoleQueryVariables): Promise { + return withWrapper(() => client.request(UpdateRoleDocument, variables)); + }, authChangeLocalPassword(variables: AuthChangeLocalPasswordQueryVariables): Promise { return withWrapper(() => client.request(AuthChangeLocalPasswordDocument, variables)); }, @@ -2998,9 +3111,6 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getPermissionsList(variables?: GetPermissionsListQueryVariables): Promise { return withWrapper(() => client.request(GetPermissionsListDocument, variables)); }, - getRolesList(variables?: GetRolesListQueryVariables): Promise { - return withWrapper(() => client.request(GetRolesListDocument, variables)); - }, getUserGrantedConnections(variables?: GetUserGrantedConnectionsQueryVariables): Promise { return withWrapper(() => client.request(GetUserGrantedConnectionsDocument, variables)); }, @@ -3034,12 +3144,18 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getConnections(variables: GetConnectionsQueryVariables): Promise { return withWrapper(() => client.request(GetConnectionsDocument, variables)); }, + getSubjectConnectionAccess(variables?: GetSubjectConnectionAccessQueryVariables): Promise { + return withWrapper(() => client.request(GetSubjectConnectionAccessDocument, variables)); + }, searchDatabases(variables: SearchDatabasesQueryVariables): Promise { return withWrapper(() => client.request(SearchDatabasesDocument, variables)); }, setConnectionAccess(variables: SetConnectionAccessQueryVariables): Promise { return withWrapper(() => client.request(SetConnectionAccessDocument, variables)); }, + setSubjectConnectionAccess(variables: SetSubjectConnectionAccessQueryVariables): Promise { + return withWrapper(() => client.request(SetSubjectConnectionAccessDocument, variables)); + }, updateConnectionConfiguration(variables: UpdateConnectionConfigurationQueryVariables): Promise { return withWrapper(() => client.request(UpdateConnectionConfigurationDocument, variables)); }, diff --git a/webapp/packages/core-theming/src/styles/_table.scss b/webapp/packages/core-theming/src/styles/_table.scss index 9751dc5e42..df826705ea 100644 --- a/webapp/packages/core-theming/src/styles/_table.scss +++ b/webapp/packages/core-theming/src/styles/_table.scss @@ -57,7 +57,7 @@ } &[use|disabled] { - opacity: .85; + opacity: 0.85; } } @@ -89,6 +89,12 @@ padding: 0 16px; transition: padding ease-in-out 0.24s; + &[use|ellipsis] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + &[use|centerContent] > td-flex { align-items: center; justify-content: center; diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index 17ecf54ff0..c43e07b289 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -25,3 +25,4 @@ export * from './isValidUrl'; export * from './isImageFormat'; export * from './getCookies'; export * from './getUniqueName'; +export * from './isMapsEqual'; diff --git a/webapp/packages/core-utils/src/isArraysEqual.ts b/webapp/packages/core-utils/src/isArraysEqual.ts index 16a3f1ad52..e02ceb5bb2 100644 --- a/webapp/packages/core-utils/src/isArraysEqual.ts +++ b/webapp/packages/core-utils/src/isArraysEqual.ts @@ -11,5 +11,5 @@ export function isArraysEqual(first: T[], second: T[]): boolean { return false; } - return !first.some(region => !second.includes(region)); + return !first.some(value => !second.includes(value)); } diff --git a/webapp/packages/core-utils/src/isMapsEqual.ts b/webapp/packages/core-utils/src/isMapsEqual.ts new file mode 100644 index 0000000000..6ead9e44dd --- /dev/null +++ b/webapp/packages/core-utils/src/isMapsEqual.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export function isMapsEqual(map1: Map, map2: Map): boolean { + if (map1.size !== map2.size) { + return false; + } + + for (const [key, value] of map1) { + if (!map2.has(key) || map2.get(key) !== value) { + return false; + } + } + return true; +} diff --git a/webapp/packages/plugin-authentication/public/icons/tab_role.svg b/webapp/packages/plugin-authentication/public/icons/tab_role.svg new file mode 100644 index 0000000000..d472228791 --- /dev/null +++ b/webapp/packages/plugin-authentication/public/icons/tab_role.svg @@ -0,0 +1,3 @@ + + + diff --git a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx index b007dce76b..bf2a17700d 100644 --- a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx @@ -16,9 +16,9 @@ import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; import { AuthConfigurationsTable } from './AuthConfigurationsTable/AuthConfigurationsTable'; +import { useConfigurationsTable } from './AuthConfigurationsTable/useConfigurationsTable'; import { CreateAuthConfiguration } from './CreateAuthConfiguration'; import { CreateAuthConfigurationService } from './CreateAuthConfigurationService'; -import { useConfigurationsTable } from './useConfigurationsTable'; const styles = composes( css` diff --git a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx index 7db66d6e8b..aeff1fe365 100644 --- a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx @@ -59,9 +59,9 @@ export const AuthConfiguration = observer(function AuthConfiguration({ co - {configuration.displayName} + {configuration.displayName} {configuration.providerId} - {configuration.description || ''} + {configuration.description || ''} diff --git a/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/useConfigurationsTable.tsx b/webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx similarity index 100% rename from webapp/packages/plugin-authentication/src/Administration/IdentityProviders/useConfigurationsTable.tsx rename to webapp/packages/plugin-authentication/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleContext.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleContext.ts new file mode 100644 index 0000000000..c7714f27c3 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleContext.ts @@ -0,0 +1,15 @@ +/* + * 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 type { RoleInfo } from '@cloudbeaver/core-authentication'; + +export function roleContext(): RoleInfo { + return { + roleId: '', + }; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleFormConfigureContext.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleFormConfigureContext.ts new file mode 100644 index 0000000000..5dc5e3f83c --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleFormConfigureContext.ts @@ -0,0 +1,25 @@ +/* + * 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 type { RoleInfo } from '@cloudbeaver/core-authentication'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; + +import type { IRoleFormState } from '../IRoleFormProps'; + +export interface IRoleFormConfigureContext { + readonly info: RoleInfo | undefined; +} + +export function roleFormConfigureContext( + contexts: IExecutionContextProvider, + state: IRoleFormState +): IRoleFormConfigureContext { + return { + info: state.info, + }; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleFormStateContext.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleFormStateContext.ts new file mode 100644 index 0000000000..3e3c0e009b --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/Contexts/roleFormStateContext.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +export interface IRoleFormStateInfo { + edited: boolean; + disabled: boolean; + readonly: boolean; + statusMessage: string | null; +} + +export interface IRoleFormStateContext extends IRoleFormStateInfo { + setStatusMessage: (message: string | null) => void; +} + +export function roleFormStateContext(): IRoleFormStateContext { + return { + edited: false, + disabled: false, + readonly: false, + statusMessage: null, + setStatusMessage(message: string | null) { + this.statusMessage = message; + }, + }; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/CreateRole.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/CreateRole.tsx new file mode 100644 index 0000000000..70d3c29f77 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/CreateRole.tsx @@ -0,0 +1,82 @@ +/* + * 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 { IconButton } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { Translate } from '@cloudbeaver/core-localization'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { CreateRoleService } from './CreateRoleService'; +import { RoleForm } from './RoleForm'; + +const styles = composes( + css` + title-bar { + composes: theme-border-color-background from global; + } + + role-create-content { + composes: theme-background-secondary theme-text-on-secondary from global; + } + `, + css` + role-create { + display: flex; + flex-direction: column; + height: 660px; + overflow: hidden; + } + + title-bar { + composes: theme-typography--headline6 from global; + box-sizing: border-box; + padding: 16px 24px; + align-items: center; + display: flex; + font-weight: 400; + flex: auto 0 0; + } + + role-create-content { + position: relative; + display: flex; + flex-direction: column; + flex: 1; + overflow: auto; + } + + fill { + flex: 1; + } + ` +); + +export const CreateRole: React.FC = observer(function CreateRole() { + const style = useStyles(styles); + const service = useService(CreateRoleService); + + if (!service.data) { + return null; + } + + return styled(style)( + + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/CreateRoleService.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/CreateRoleService.ts new file mode 100644 index 0000000000..eb6f20ec94 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/CreateRoleService.ts @@ -0,0 +1,54 @@ +/* + * 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 { observable, makeObservable } from 'mobx'; + +import { RolesResource } from '@cloudbeaver/core-authentication'; +import { injectable } from '@cloudbeaver/core-di'; + +import type { IRoleFormState } from './IRoleFormProps'; +import { RoleFormService } from './RoleFormService'; +import { RoleFormState } from './RoleFormState'; +import { RolesAdministrationNavService } from './RolesAdministrationNavService'; + +@injectable() +export class CreateRoleService { + disabled = false; + data: IRoleFormState | null; + + constructor( + private readonly rolesAdministrationNavService: RolesAdministrationNavService, + private readonly roleFormService: RoleFormService, + private readonly rolesResource: RolesResource + ) { + makeObservable(this, { + data: observable, + disabled: observable, + }); + + this.data = null; + + this.cancelCreate = this.cancelCreate.bind(this); + this.create = this.create.bind(this); + } + + cancelCreate(): void { + this.rolesAdministrationNavService.navToRoot(); + } + + fillData(): void { + this.data = new RoleFormState( + this.roleFormService, + this.rolesResource + ); + } + + create(): void { + this.rolesAdministrationNavService.navToCreate(); + } +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/ConnectionList.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/ConnectionList.tsx new file mode 100644 index 0000000000..afaa133d11 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/ConnectionList.tsx @@ -0,0 +1,131 @@ +/* + * 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; +import styled, { css } from 'reshadow'; + +import { + Table, + TableBody, + TableItem, + TableColumnValue, + BASE_CONTAINERS_STYLES, + Group, + Button, + useObjectRef, + getComputed, +} from '@cloudbeaver/core-blocks'; +import { DBDriverResource } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { getFilteredConnections } from './getFilteredConnections'; +import { GrantedConnectionsTableHeader, IFilterState } from './GrantedConnectionsTableHeader/GrantedConnectionsTableHeader'; +import { GrantedConnectionsTableInnerHeader } from './GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader'; +import { GrantedConnectionsTableItem } from './GrantedConnectionsTableItem'; + +const styles = composes( + css` + Table { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + Group { + position: relative; + } + Group, container, table-container { + height: 100%; + } + container { + display: flex; + flex-direction: column; + width: 100%; + } + table-container { + overflow: auto; + } + GrantedConnectionsTableHeader { + flex: 0 0 auto; + } + ` +); + +interface Props { + connectionList: DatabaseConnectionFragment[]; + grantedSubjects: string[]; + onGrant: (subjectIds: string[]) => void; + disabled: boolean; +} + +export const ConnectionList = observer(function ConnectionList({ + connectionList, + grantedSubjects, + onGrant, + disabled, +}) { + const props = useObjectRef({ onGrant }); + const style = useStyles(styles, BASE_CONTAINERS_STYLES); + const translate = useTranslate(); + + const driversResource = useService(DBDriverResource); + + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const grant = useCallback(() => { + const selectedList = Array.from(selectedSubjects.entries()).filter(([key, value]) => value).map(([key]) => key); + props.onGrant(selectedList); + selectedSubjects.clear(); + }, []); + + const connections = getFilteredConnections(connectionList, filterState.filterValue); + + return styled(style)( + + + + + + + + + + {!connections.length && filterState.filterValue && ( + + + {translate('connections_connection_access_filter_no_result')} + + + )} + {connections.map(connection => { + const driver = driversResource.get(connection.driverId); + return ( + + ); + })} + +
+
+
+
+ ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnections.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnections.tsx new file mode 100644 index 0000000000..47fcf450a5 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnections.tsx @@ -0,0 +1,145 @@ +/* + * 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 { computed } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useEffect, useMemo } from 'react'; +import styled, { css } from 'reshadow'; + +import { + BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, + IconOrImage, Loader, TabContainerPanelComponent, + TextPlaceholder, useMapResource, useTab, +} from '@cloudbeaver/core-blocks'; +import { ConnectionsResource, DBDriverResource, isCloudConnection } from '@cloudbeaver/core-connections'; +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import type { IRoleFormProps } from '../IRoleFormProps'; +import { ConnectionList } from './ConnectionList'; +import { GrantedConnectionList } from './GrantedConnectionsList'; +import { useGrantedConnections } from './useGrantedConnections'; + +const styles = css` + ColoredContainer { + flex: 1; + height: 100%; + box-sizing: border-box; + } + Group { + max-height: 100%; + 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, +}) { + const style = useStyles(BASE_CONTAINERS_STYLES, styles); + const translate = useTranslate(); + + const { state, edit, grant, load, revoke } = 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]); + + useEffect(() => { + if (selected && !state.loaded) { + load(); + } + }, [selected, state.loaded, load]); + + if (!selected) { + return null; + } + + let infoItem: IInfoItem | 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', + }; + } + + if (unsaved) { + infoItem = { + text: 'connections_connection_access_save_reminder', + icon: '/icons/info_icon.svg', + }; + } + + return styled(style)( + + {() => styled(style)( + + {!connections.resource.values.length ? ( + + {translate('administration_roles_role_granted_connections_empty')} + + ) : ( + <> + {infoItem && ( + + + {translate(infoItem.text)} + + )} + + + {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 new file mode 100644 index 0000000000..2b9e6c4b10 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsList.tsx @@ -0,0 +1,141 @@ +/* + * 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; +import styled, { css } from 'reshadow'; + +import { + Table, + TableBody, + TableItem, + TableColumnValue, + BASE_CONTAINERS_STYLES, + Group, + Button, + useObjectRef +} from '@cloudbeaver/core-blocks'; +import { getComputed } from '@cloudbeaver/core-blocks'; +import { DBDriverResource } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; +import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { getFilteredConnections } from './getFilteredConnections'; +import { GrantedConnectionsTableHeader, IFilterState } from './GrantedConnectionsTableHeader/GrantedConnectionsTableHeader'; +import { GrantedConnectionsTableInnerHeader } from './GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader'; +import { GrantedConnectionsTableItem } from './GrantedConnectionsTableItem'; + +const styles = composes( + css` + Table { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + Group { + position: relative; + } + Group, container, table-container { + height: 100%; + } + container { + display: flex; + flex-direction: column; + width: 100%; + } + GrantedConnectionsTableHeader { + flex: 0 0 auto; + } + table-container { + overflow: auto; + } + ` +); + +interface Props { + grantedConnections: DatabaseConnectionFragment[]; + disabled: boolean; + onRevoke: (subjectIds: string[]) => void; + onEdit: () => void; +} + +export const GrantedConnectionList = observer(function GrantedConnectionList({ + grantedConnections, + disabled, + onRevoke, + onEdit, +}) { + const props = useObjectRef({ onRevoke, onEdit }); + const style = useStyles(styles, BASE_CONTAINERS_STYLES); + const translate = useTranslate(); + + const driversResource = useService(DBDriverResource); + + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const revoke = useCallback(() => { + const selectedList = Array.from(selectedSubjects.entries()).filter(([key, value]) => value).map(([key]) => key); + props.onRevoke(selectedList); + selectedSubjects.clear(); + }, []); + + const connections = getFilteredConnections(grantedConnections, filterState.filterValue); + + let tableInfoText: TLocalizationToken | null = null; + if (!connections.length) { + if (filterState.filterValue) { + tableInfoText = 'connections_connection_access_filter_no_result'; + } else { + tableInfoText = 'connections_connection_access_empty_table_placeholder'; + } + } + + return styled(style)( + + + + + + + + + + + + {tableInfoText && ( + + {translate(tableInfoText)} + + )} + + {connections.map(connection => { + const driver = driversResource.get(connection.driverId); + return ( + + ); + })} + +
+
+
+
+ ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTabService.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTabService.ts new file mode 100644 index 0000000000..b84f2cd813 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTabService.ts @@ -0,0 +1,102 @@ +/* + * 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 { RolesResource } from '@cloudbeaver/core-authentication'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; +import { isArraysEqual, MetadataValueGetter } from '@cloudbeaver/core-utils'; + +import { roleContext } from '../Contexts/roleContext'; +import type { IRoleFormProps, IRoleFormSubmitData } from '../IRoleFormProps'; +import { RoleFormService } from '../RoleFormService'; +import { GrantedConnections } from './GrantedConnections'; +import type { IGrantedConnectionsTabState } from './IGrantedConnectionsTabState'; + +@injectable() +export class GrantedConnectionsTabService extends Bootstrap { + private key: string; + + constructor( + private readonly roleFormService: RoleFormService, + private readonly rolesResource: RolesResource, + private readonly graphQLService: GraphQLService, + private readonly notificationService: NotificationService + ) { + super(); + this.key = 'granted-connections'; + } + + register(): void { + this.roleFormService.tabsContainer.add({ + key: this.key, + name: 'administration_roles_role_granted_connections_tab_title', + title: 'administration_roles_role_granted_connections_tab_title', + order: 3, + stateGetter: context => this.stateGetter(context), + panel: () => GrantedConnections, + }); + + this.roleFormService.formSubmittingTask.addHandler(this.save.bind(this)); + } + + load(): Promise | void { } + + private stateGetter(context: IRoleFormProps): MetadataValueGetter { + return () => ({ + loading: false, + loaded: false, + editing: false, + grantedSubjects: [], + initialGrantedSubjects: [], + }); + } + + private async save( + data: IRoleFormSubmitData, + contexts: IExecutionContextProvider + ) { + const config = contexts.getContext(roleContext); + const status = contexts.getContext(this.roleFormService.configurationStatusContext); + + if (!status.saved) { + return; + } + + const state = this.roleFormService.tabsContainer.getTabState( + data.state.partsState, + this.key, + { state: data.state } + ); + + if (!config.roleId || !state.loaded) { + return; + } + + const grantInfo = await this.rolesResource.getSubjectConnectionAccess(config.roleId); + const initial = grantInfo.map(info => info.connectionId); + + const changed = !isArraysEqual(initial, state.grantedSubjects); + + if (!changed) { + return; + } + + try { + await this.graphQLService.sdk.setSubjectConnectionAccess({ + subjectId: config.roleId, + connections: state.grantedSubjects, + }); + + state.loaded = false; + } catch (exception) { + this.notificationService.logException(exception); + } + } +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx new file mode 100644 index 0000000000..a1ceaa66cb --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx @@ -0,0 +1,67 @@ +/* + * 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 { Filter } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +export interface IFilterState { + filterValue: string; +} + +interface Props { + filterState: IFilterState; + disabled: boolean; + className?: string; +} + +const styles = composes( + css` + header { + composes: theme-border-color-background theme-background-surface theme-text-on-surface from global; + } + `, + css` + buttons { + display: flex; + gap: 16px; + } + header { + overflow: hidden; + position: sticky; + top: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + gap: 16px; + border-bottom: 1px solid; + } + ` +); + +export const GrantedConnectionsTableHeader = observer(function GrantedConnectionsTableHeader({ filterState, disabled, className, children }) { + const translate = useTranslate(); + return styled(useStyles(styles))( +
+ + + {children} + +
+ ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx new file mode 100644 index 0000000000..8ae80ac98b --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx @@ -0,0 +1,45 @@ +/* + * 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 { TableColumnHeader, TableHeader } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +interface Props { + className?: string; +} + +const styles = composes( + css` + TableHeader { + composes: theme-background-surface from global; + } + `, + css` + TableHeader { + position: sticky; + top: 0; + z-index: 1; + } + ` +); + +export const GrantedConnectionsTableInnerHeader = observer(function GrantedConnectionsTableInnerHeader({ className }) { + const translate = useTranslate(); + return styled(useStyles(styles))( + + + + {translate('connections_connection_name')} + {translate('connections_connection_address')} + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableItem.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableItem.tsx new file mode 100644 index 0000000000..97b7dca4c7 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/GrantedConnectionsTableItem.tsx @@ -0,0 +1,52 @@ +/* + * 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 { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; + +interface Props { + id: any; + name: string; + disabled: boolean; + host?: string; + icon?: string; + iconTooltip?: TLocalizationToken; + className?: string; +} + +const style = css` + StaticImage { + display: flex; + width: 24px; + } +`; + +export const GrantedConnectionsTableItem = observer(function GrantedConnectionsTableItem({ + id, name, host, icon, iconTooltip, disabled, className, +}) { + const translate = useTranslate(); + + return styled(style)( + + + + + {icon && } + {name} + {host && host} + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/IGrantedConnectionsTabState.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/IGrantedConnectionsTabState.ts new file mode 100644 index 0000000000..80175967f5 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/IGrantedConnectionsTabState.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export interface IGrantedConnectionsTabState { + loading: boolean; + loaded: boolean; + grantedSubjects: string[]; + initialGrantedSubjects: string[]; + editing: boolean; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/getFilteredConnections.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/getFilteredConnections.ts new file mode 100644 index 0000000000..88d9c66f05 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/getFilteredConnections.ts @@ -0,0 +1,25 @@ +/* + * 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 { isCloudConnection } from '@cloudbeaver/core-connections'; +import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; + +/** + * @param {DatabaseConnectionFragment[]} connections + * @param {string} filter + */ +export function getFilteredConnections( + connections: DatabaseConnectionFragment[], + filter: string +): DatabaseConnectionFragment[] { + return connections + .filter( + connection => connection.name.toLowerCase().includes(filter.toLowerCase()) && !isCloudConnection(connection) + ) + .sort((a, b) => (a.name).localeCompare(b.name)); +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/useGrantedConnections.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/useGrantedConnections.tsx new file mode 100644 index 0000000000..157ee8e92f --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedConnections/useGrantedConnections.tsx @@ -0,0 +1,64 @@ +/* + * 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 { RoleInfo, RolesResource } from '@cloudbeaver/core-authentication'; +import { useObjectRef, useTabState } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; + +import type { RoleFormMode } from '../IRoleFormProps'; +import type { IGrantedConnectionsTabState } from './IGrantedConnectionsTabState'; + +interface IConnectionAccessState { + state: IGrantedConnectionsTabState; + revoke: (subjectIds: string[]) => void; + grant: (subjectIds: string[]) => void; + edit: () => void; + load: () => Promise; +} + +export function useGrantedConnections(role: RoleInfo, mode: RoleFormMode): IConnectionAccessState { + 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(); + } + + state.loaded = true; + } catch (exception) { + notificationService.logException(exception, `Error getting granted connections for "${role.roleId}"`); + } + state.loading = false; + }; + + return useObjectRef({ state, revoke, grant, edit, 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 new file mode 100644 index 0000000000..8d7968b65a --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUserList.tsx @@ -0,0 +1,138 @@ +/* + * 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; +import styled, { css } from 'reshadow'; + +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { + Table, + TableBody, + TableItem, + TableColumnValue, + BASE_CONTAINERS_STYLES, + Group, + Button, + useObjectRef, + getComputed +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { getFilteredUsers } from './getFilteredUsers'; +import { GrantedUsersTableHeader, IFilterState } from './GrantedUsersTableHeader/GrantedUsersTableHeader'; +import { GrantedUsersTableInnerHeader } from './GrantedUsersTableHeader/GrantedUsersTableInnerHeader'; +import { GrantedUsersTableItem } from './GrantedUsersTableItem'; + +const styles = composes( + css` + Table { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + Group { + position: relative; + } + Group, container, table-container { + height: 100%; + } + container { + display: flex; + flex-direction: column; + width: 100%; + } + GrantedUsersTableHeader { + flex: 0 0 auto; + } + table-container { + overflow: auto; + } + ` +); + +interface Props { + grantedUsers: AdminUserInfoFragment[]; + disabled: boolean; + onRevoke: (subjectIds: string[]) => void; + onEdit: () => void; +} + +export const GrantedUserList = observer(function GrantedUserList({ + grantedUsers, + disabled, + onRevoke, + onEdit, +}) { + const props = useObjectRef({ onRevoke, onEdit }); + const style = useStyles(styles, BASE_CONTAINERS_STYLES); + const translate = useTranslate(); + + const usersResource = useService(UsersResource); + + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const revoke = useCallback(() => { + const selectedList = Array.from(selectedSubjects.entries()).filter(([key, value]) => value).map(([key]) => key); + props.onRevoke(selectedList); + selectedSubjects.clear(); + }, []); + + const users = getFilteredUsers(grantedUsers, filterState.filterValue); + + let tableInfoText: TLocalizationToken | null = null; + if (!users.length) { + if (filterState.filterValue) { + tableInfoText = 'connections_connection_access_filter_no_result'; + } else { + tableInfoText = 'connections_connection_access_empty_table_placeholder'; + } + } + + return styled(style)( + + + + + + + + + + + + {tableInfoText && ( + + {translate(tableInfoText)} + + )} + + {users.map(user => ( + + ))} + +
+
+
+
+ ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsers.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsers.tsx new file mode 100644 index 0000000000..9404402b8a --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsers.tsx @@ -0,0 +1,136 @@ +/* + * 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 { computed } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useEffect, useMemo } from 'react'; +import styled, { css } from 'reshadow'; + +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { + BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, + IconOrImage, Loader, TabContainerPanelComponent, + TextPlaceholder, useMapResource, useTab, +} from '@cloudbeaver/core-blocks'; +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import type { IRoleFormProps } from '../IRoleFormProps'; +import { GrantedUserList } from './GrantedUserList'; +import { useGrantedUsers } from './useGrantedUsers'; +import { UserList } from './UserList'; + +const styles = css` + ColoredContainer { + flex: 1; + height: 100%; + box-sizing: border-box; + } + Group { + max-height: 100%; + 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, +}) { + const style = useStyles(BASE_CONTAINERS_STYLES, styles); + const translate = useTranslate(); + + const { state, edit, grant, load, revoke } = 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]); + + useEffect(() => { + if (selected && !state.loaded) { + load(); + } + }, [selected, state.loaded, load]); + + 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 ? ( + + {translate('administration_roles_role_granted_users_empty')} + + ) : ( + <> + {infoItem && ( + + + {translate(infoItem.text)} + + )} + + + {state.editing && ( + + )} + + + )} + + )} + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTabService.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTabService.ts new file mode 100644 index 0000000000..b4ee6188ac --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTabService.ts @@ -0,0 +1,120 @@ +/* + * 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 { RolesResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { isArraysEqual, MetadataValueGetter } from '@cloudbeaver/core-utils'; + +import { roleContext } from '../Contexts/roleContext'; +import type { IRoleFormProps, IRoleFormSubmitData } from '../IRoleFormProps'; +import { RoleFormService } from '../RoleFormService'; +import { GrantedUsers } from './GrantedUsers'; +import type { IGrantedUsersTabState } from './IGrantedUsersTabState'; + +@injectable() +export class GrantedUsersTabService extends Bootstrap { + private key: string; + + constructor( + private readonly roleFormService: RoleFormService, + private readonly usersResource: UsersResource, + private readonly rolesResource: RolesResource, + private readonly notificationService: NotificationService + ) { + super(); + this.key = 'granted-users'; + } + + register(): void { + this.roleFormService.tabsContainer.add({ + key: this.key, + name: 'administration_roles_role_granted_users_tab_title', + title: 'administration_roles_role_granted_users_tab_title', + order: 2, + stateGetter: context => this.stateGetter(context), + panel: () => GrantedUsers, + }); + + this.roleFormService.formSubmittingTask.addHandler(this.save.bind(this)); + } + + load(): void { } + + private stateGetter(context: IRoleFormProps): MetadataValueGetter { + return () => ({ + loading: false, + loaded: false, + editing: false, + grantedUsers: [], + initialGrantedUsers: [], + }); + } + + private async save( + data: IRoleFormSubmitData, + contexts: IExecutionContextProvider + ) { + const config = contexts.getContext(roleContext); + const status = contexts.getContext(this.roleFormService.configurationStatusContext); + + if (!status.saved) { + return; + } + + const state = this.roleFormService.tabsContainer.getTabState( + data.state.partsState, + this.key, + { state: data.state } + ); + + if (!config.roleId || !state.loaded) { + return; + } + + const initial = await this.rolesResource.loadGrantedUsers(config.roleId); + + const changed = !isArraysEqual(initial, state.grantedUsers); + + if (!changed) { + return; + } + + const granted: string[] = []; + const revoked: string[] = []; + + const revokedUsers = initial.filter(user => !state.grantedUsers.includes(user)); + + try { + for (const user of revokedUsers) { + await this.usersResource.revokeRole(user, config.roleId); + revoked.push(user); + } + + for (const user of state.grantedUsers) { + if (!initial.includes(user)) { + await this.usersResource.grantRole(user, config.roleId); + granted.push(user); + } + } + + state.loaded = false; + } catch (exception) { + this.notificationService.logException(exception); + } + + if (granted.length) { + status.info(`Granted users: "${granted.join(', ')}"`); + } + + if (revoked.length) { + status.info(`Revoked users: "${revoked.join(', ')}"`); + } + } +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx new file mode 100644 index 0000000000..459835e65b --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx @@ -0,0 +1,67 @@ +/* + * 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 { Filter } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +export interface IFilterState { + filterValue: string; +} + +interface Props { + filterState: IFilterState; + disabled: boolean; + className?: string; +} + +const styles = composes( + css` + header { + composes: theme-border-color-background theme-background-surface theme-text-on-surface from global; + } + `, + css` + buttons { + display: flex; + gap: 16px; + } + header { + overflow: hidden; + position: sticky; + top: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + gap: 16px; + border-bottom: 1px solid; + } + ` +); + +export const GrantedUsersTableHeader = observer(function GrantedUsersTableHeader({ filterState, disabled, className, children }) { + const translate = useTranslate(); + return styled(useStyles(styles))( +
+ + + {children} + +
+ ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx new file mode 100644 index 0000000000..b0b7c7d852 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx @@ -0,0 +1,44 @@ +/* + * 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 { TableColumnHeader, TableHeader } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +interface Props { + className?: string; +} + +const styles = composes( + css` + TableHeader { + composes: theme-background-surface from global; + } + `, + css` + TableHeader { + position: sticky; + top: 0; + z-index: 1; + } + ` +); + +export const GrantedUsersTableInnerHeader = observer(function GrantedUsersTableInnerHeader({ className }) { + const translate = useTranslate(); + return styled(useStyles(styles))( + + + + {translate('administration_roles_role_granted_users_user_id')} + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableItem.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableItem.tsx new file mode 100644 index 0000000000..0ced640aba --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/GrantedUsersTableItem.tsx @@ -0,0 +1,49 @@ +/* + * 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 { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; + +interface Props { + id: any; + name: string; + icon: string; + disabled: boolean; + iconTooltip?: TLocalizationToken; + className?: string; +} + +const style = css` + StaticImage { + display: flex; + width: 24px; + } +`; + +export const GrantedUsersTableItem = observer(function GrantedUsersTableItem({ + id, name, icon, iconTooltip, disabled, className, +}) { + const translate = useTranslate(); + return styled(style)( + + + + + + {name} + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/IGrantedUsersTabState.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/IGrantedUsersTabState.ts new file mode 100644 index 0000000000..eb4730b65d --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/IGrantedUsersTabState.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export interface IGrantedUsersTabState { + loading: boolean; + loaded: boolean; + grantedUsers: string[]; + initialGrantedUsers: string[]; + editing: boolean; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/UserList.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/UserList.tsx new file mode 100644 index 0000000000..079134056b --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/UserList.tsx @@ -0,0 +1,128 @@ +/* + * 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; +import styled, { css } from 'reshadow'; + +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { + Table, + TableBody, + TableItem, + TableColumnValue, + BASE_CONTAINERS_STYLES, + Group, + Button, + useObjectRef, + getComputed, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { getFilteredUsers } from './getFilteredUsers'; +import { GrantedUsersTableHeader, IFilterState } from './GrantedUsersTableHeader/GrantedUsersTableHeader'; +import { GrantedUsersTableInnerHeader } from './GrantedUsersTableHeader/GrantedUsersTableInnerHeader'; +import { GrantedUsersTableItem } from './GrantedUsersTableItem'; + +const styles = composes( + css` + Table { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + Group { + position: relative; + } + Group, container, table-container { + height: 100%; + } + container { + display: flex; + flex-direction: column; + width: 100%; + } + table-container { + overflow: auto; + } + GrantedUsersTableHeader { + flex: 0 0 auto; + } + ` +); + +interface Props { + userList: AdminUserInfoFragment[]; + grantedUsers: string[]; + onGrant: (subjectIds: string[]) => void; + disabled: boolean; +} + +export const UserList = observer(function UserList({ + userList, + grantedUsers, + onGrant, + disabled, +}) { + const props = useObjectRef({ onGrant }); + const style = useStyles(styles, BASE_CONTAINERS_STYLES); + const translate = useTranslate(); + + const usersResource = useService(UsersResource); + + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const grant = useCallback(() => { + const selectedList = Array.from(selectedSubjects.entries()).filter(([key, value]) => value).map(([key]) => key); + props.onGrant(selectedList); + selectedSubjects.clear(); + }, []); + + const users = getFilteredUsers(userList, filterState.filterValue); + + return styled(style)( + + + + + + + + + + {!users.length && filterState.filterValue && ( + + + {translate('connections_connection_access_filter_no_result')} + + + )} + {users.map(user => ( + + ))} + +
+
+
+
+ ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/getFilteredUsers.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/getFilteredUsers.ts new file mode 100644 index 0000000000..371d909351 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/getFilteredUsers.ts @@ -0,0 +1,19 @@ +/* + * 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 type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; + +/** + * @param {AdminUserInfoFragment[]} users + * @param {string} filter + */ +export function getFilteredUsers(users: AdminUserInfoFragment[], filter: string): AdminUserInfoFragment[] { + return users + .filter(user => user.userId.toLowerCase().includes(filter.toLowerCase())) + .sort((a, b) => (a.userId).localeCompare(b.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 new file mode 100644 index 0000000000..4073e21f5c --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/GrantedUsers/useGrantedUsers.tsx @@ -0,0 +1,65 @@ +/* + * 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 { RoleInfo, RolesResource } from '@cloudbeaver/core-authentication'; +import { useObjectRef, useTabState } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; + +import type { RoleFormMode } from '../IRoleFormProps'; +import type { IGrantedUsersTabState } from './IGrantedUsersTabState'; + +interface IConnectionAccessState { + state: IGrantedUsersTabState; + revoke: (subjectIds: string[]) => void; + grant: (subjectIds: string[]) => void; + edit: () => void; + load: () => Promise; +} + +export function useGrantedUsers(role: RoleInfo, mode: RoleFormMode): IConnectionAccessState { + 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(); + } + + state.loaded = true; + } catch (exception) { + notificationService.logException(exception, "Can't load users info"); + } + state.loading = false; + }; + + return useObjectRef({ state, revoke, grant, edit, load }); +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/IRoleFormProps.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/IRoleFormProps.ts new file mode 100644 index 0000000000..1ae1b3c92b --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/IRoleFormProps.ts @@ -0,0 +1,50 @@ +/* + * 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 type { RoleInfo } from '@cloudbeaver/core-authentication'; +import type { IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; +import type { CachedMapResource } from '@cloudbeaver/core-sdk'; +import type { MetadataMap } from '@cloudbeaver/core-utils'; + +export type RoleFormMode = 'edit' | 'create'; + +export interface IRoleFormState { + mode: RoleFormMode; + config: RoleInfo; + partsState: MetadataMap; + + readonly info: RoleInfo | undefined; + readonly statusMessage: string | null; + readonly disabled: boolean; + readonly readonly: boolean; + readonly loading: boolean; + + readonly submittingTask: IExecutorHandlersCollection; + readonly resource: CachedMapResource; + + readonly load: () => Promise; + readonly loadRoleInfo: () => Promise; + readonly save: () => Promise; + readonly setOptions: ( + mode: RoleFormMode, + ) => this; +} + +export interface IRoleFormProps { + state: IRoleFormState; + onCancel?: () => void; +} + +export interface IRoleFormFillConfigData { + updated: boolean; + state: IRoleFormState; +} + +export interface IRoleFormSubmitData { + state: IRoleFormState; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/Options/RoleOptions.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/Options/RoleOptions.tsx new file mode 100644 index 0000000000..7fa538b748 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/Options/RoleOptions.tsx @@ -0,0 +1,77 @@ +/* + * 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 { useRef } from 'react'; +import styled, { css } from 'reshadow'; + +import { + BASE_CONTAINERS_STYLES, ColoredContainer, Group, + InputFieldNew, SubmittingForm, TabContainerPanelComponent, TextareaNew, +} from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import type { IRoleFormProps } from '../IRoleFormProps'; + +const styles = css` + SubmittingForm { + flex: 1; + overflow: auto; + } +`; + +export const RoleOptions: TabContainerPanelComponent = observer(function RoleOptions({ + state, +}) { + const formRef = useRef(null); + + const translate = useTranslate(); + const style = useStyles(BASE_CONTAINERS_STYLES, styles); + const edit = state.mode === 'edit'; + + return styled(style)( + + + + + {translate('administration_roles_role_id')} + + + {translate('administration_roles_role_name')} + + + {translate('administration_roles_role_description')} + + + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/Options/RoleOptionsTabService.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/Options/RoleOptionsTabService.ts new file mode 100644 index 0000000000..9af1f859c4 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/Options/RoleOptionsTabService.ts @@ -0,0 +1,146 @@ +/* + * 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 { RolesResource } from '@cloudbeaver/core-authentication'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { getUniqueName } from '@cloudbeaver/core-utils'; + +import { roleContext } from '../Contexts/roleContext'; +import type { IRoleFormFillConfigData, IRoleFormSubmitData } from '../IRoleFormProps'; +import { RoleFormService } from '../RoleFormService'; +import { RoleOptions } from './RoleOptions'; + +@injectable() +export class RoleOptionsTabService extends Bootstrap { + constructor( + private readonly roleFormService: RoleFormService, + private readonly roleResource: RolesResource, + ) { + super(); + } + + register(): void { + this.roleFormService.tabsContainer.add({ + key: 'options', + name: 'Options', + order: 1, + panel: () => RoleOptions, + }); + + this.roleFormService.prepareConfigTask + .addHandler(this.prepareConfig.bind(this)); + + this.roleFormService.formValidationTask + .addHandler(this.validate.bind(this)); + + this.roleFormService.formSubmittingTask + .addHandler(this.save.bind(this)); + + this.roleFormService.fillConfigTask + .addHandler(this.fillConfig.bind(this)); + } + + load(): void { } + + private async prepareConfig( + { + state, + }: IRoleFormSubmitData, + contexts: IExecutionContextProvider + ) { + const config = contexts.getContext(roleContext); + + config.roleId = state.config.roleId; + + if (state.config.roleName) { + config.roleName = state.config.roleName.trim(); + + if (state.mode === 'create') { + const roleNames = this.roleResource.values.map(role => role.roleName).filter(Boolean) as string[]; + config.roleName = getUniqueName(config.roleName, roleNames); + } + } + + if (state.config.description) { + config.description = state.config.description; + } + } + + private async validate( + { + state, + }: IRoleFormSubmitData, + contexts: IExecutionContextProvider + ) { + const validation = contexts.getContext(this.roleFormService.configurationValidationContext); + + if (state.mode === 'create') { + if (!state.config.roleId.trim()) { + validation.error("Field 'Role ID' can't be empty"); + } + + if (this.roleResource.has(state.config.roleId)) { + validation.error(`A role with ID "${state.config.roleId}" already exists`); + } + } + } + + private async save( + { + state, + }: IRoleFormSubmitData, + contexts: IExecutionContextProvider + ) { + const status = contexts.getContext(this.roleFormService.configurationStatusContext); + const config = contexts.getContext(roleContext); + + const create = state.mode === 'create'; + + try { + if (create) { + const role = await this.roleResource.createRole(config); + status.info('Role is created'); + status.info(role.roleId); + } else { + const role = await this.roleResource.updateRole(config); + status.info('Role is updated'); + status.info(role.roleId); + } + } catch (exception) { + if (create) { + status.error('administration_roles_role_create_error', exception); + } else { + status.error('administration_roles_role_save_error', exception); + } + } + } + + private fillConfig( + { state, updated }: IRoleFormFillConfigData, + contexts: IExecutionContextProvider + ) { + if (!updated) { + return; + } + + if (!state.info) { + return; + } + + if (state.info.roleId) { + state.config.roleId = state.info.roleId; + } + if (state.info.roleName) { + state.config.roleName = state.info.roleName; + } + if (state.info.description) { + state.config.description = state.info.description; + } + } +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RoleForm.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RoleForm.tsx new file mode 100644 index 0000000000..4384bf7965 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RoleForm.tsx @@ -0,0 +1,181 @@ +/* + * 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 { useEffect } from 'react'; +import styled, { css } from 'reshadow'; + +import type { RoleInfo } from '@cloudbeaver/core-authentication'; +import { + TabsState, TabList, UNDERLINE_TAB_STYLES, TabPanelList, Placeholder, + useObjectRef, useExecutor, BASE_CONTAINERS_STYLES, IconOrImage +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { roleContext } from './Contexts/roleContext'; +import type { IRoleFormState } from './IRoleFormProps'; +import { RoleFormService } from './RoleFormService'; + +const tabsStyles = css` + TabList { + position: relative; + flex-shrink: 0; + align-items: center; + } + Tab { + height: 46px!important; + text-transform: uppercase; + font-weight: 500 !important; + } +`; + +const topBarStyles = composes( + css` + role-top-bar { + composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; + } + `, + css` + role-top-bar { + position: relative; + display: flex; + padding-top: 16px; + + &:before { + content: ''; + position: absolute; + bottom: 0; + width: 100%; + border-bottom: solid 2px; + border-color: inherit; + } + } + role-top-bar-tabs { + flex: 1; + } + + role-top-bar-actions { + display: flex; + align-items: center; + padding: 0 24px; + gap: 16px; + } + + role-status-message { + composes: theme-typography--caption from global; + height: 24px; + padding: 0 16px; + display: flex; + align-items: center; + gap: 8px; + + & IconOrImage { + height: 24px; + width: 24px; + } + } + ` +); + +const formStyles = composes( + css` + box { + composes: theme-background-secondary theme-text-on-secondary from global; + } + + content-box { + composes: theme-background-secondary theme-border-color-background from global; + } + `, + css` + box { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: auto; + } + content-box { + position: relative; + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; + } + ` +); + +interface Props { + state: IRoleFormState; + onCancel?: () => void; + onSave?: (role: RoleInfo) => void; + className?: string; +} + +export const RoleForm = observer(function RoleForm({ + state, + onCancel, + onSave = () => {}, + className, +}) { + const translate = useTranslate(); + const props = useObjectRef({ onSave }); + const style = [tabsStyles, UNDERLINE_TAB_STYLES]; + const styles = useStyles(style, BASE_CONTAINERS_STYLES, topBarStyles, formStyles); + const service = useService(RoleFormService); + + useExecutor({ + executor: state.submittingTask, + postHandlers: [function save(data, contexts) { + const validation = contexts.getContext(service.configurationValidationContext); + const state = contexts.getContext(service.configurationStatusContext); + const config = contexts.getContext(roleContext); + + if (validation.valid && state.saved) { + props.onSave(config); + } + }], + }); + + useEffect(() => { + state.loadRoleInfo(); + }, []); + + return styled(styles)( + + + + + + {state.statusMessage && ( + <> + + {translate(state.statusMessage)} + + )} + + + + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormBaseActions.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormBaseActions.tsx new file mode 100644 index 0000000000..d141c9f967 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormBaseActions.tsx @@ -0,0 +1,45 @@ +/* + * 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 { Button, PlaceholderComponent } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; + +import type { IRoleFormProps } from './IRoleFormProps'; + +export const RoleFormBaseActions: PlaceholderComponent = observer(function RoleFormBaseActions({ + state, + onCancel, +}) { + const translate = useTranslate(); + + return ( + <> + {onCancel && ( + + )} + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormService.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormService.ts new file mode 100644 index 0000000000..b77390785b --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormService.ts @@ -0,0 +1,134 @@ +/* + * 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 { PlaceholderContainer, TabsContainer } from '@cloudbeaver/core-blocks'; +import { injectable } from '@cloudbeaver/core-di'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; +import { ExecutorHandlersCollection, ExecutorInterrupter, IExecutorHandler, IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; + +import type { IRoleFormFillConfigData, IRoleFormProps, IRoleFormState, IRoleFormSubmitData } from './IRoleFormProps'; +import { RoleFormBaseActions } from './RoleFormBaseActions'; + +export interface IRoleFormValidation { + valid: boolean; + messages: string[]; + info: (message: string) => void; + error: (message: string) => void; +} + +export interface IRoleFormStatus { + saved: boolean; + messages: string[]; + exception: Error | null; + info: (message: string) => void; + error: (message: string, exception?: Error) => void; +} + +@injectable() +export class RoleFormService { + readonly tabsContainer: TabsContainer; + readonly actionsContainer: PlaceholderContainer; + + readonly configureTask: IExecutorHandlersCollection; + readonly fillConfigTask: IExecutorHandlersCollection; + readonly prepareConfigTask: IExecutorHandlersCollection; + readonly formValidationTask: IExecutorHandlersCollection; + readonly formSubmittingTask: IExecutorHandlersCollection; + readonly formStateTask: IExecutorHandlersCollection; + + constructor( + private readonly notificationService: NotificationService, + ) { + this.tabsContainer = new TabsContainer(); + this.actionsContainer = new PlaceholderContainer(); + this.configureTask = new ExecutorHandlersCollection(); + this.fillConfigTask = new ExecutorHandlersCollection(); + this.prepareConfigTask = new ExecutorHandlersCollection(); + this.formSubmittingTask = new ExecutorHandlersCollection(); + this.formValidationTask = new ExecutorHandlersCollection(); + this.formStateTask = new ExecutorHandlersCollection(); + + this.formSubmittingTask + .before(this.formValidationTask) + .before(this.prepareConfigTask); + + this.formStateTask + .before(this.prepareConfigTask, state => ({ state, submitType: 'submit' })); + + this.formSubmittingTask.addPostHandler(this.showSubmittingStatusMessage); + this.formValidationTask.addPostHandler(this.ensureValidation); + + this.actionsContainer.add(RoleFormBaseActions); + } + + configurationValidationContext = (): IRoleFormValidation => ({ + valid: true, + messages: [], + info(message: string) { + this.messages.push(message); + }, + error(message: string) { + this.messages.push(message); + this.valid = false; + }, + }); + + configurationStatusContext = (): IRoleFormStatus => ({ + saved: true, + messages: [], + exception: null, + info(message: string) { + this.messages.push(message); + }, + error(message: string, exception: Error | null = null) { + this.messages.push(message); + this.saved = false; + this.exception = exception; + }, + }); + + private showSubmittingStatusMessage: IExecutorHandler = (data, contexts) => { + const status = contexts.getContext(this.configurationStatusContext); + + if (!status.saved) { + ExecutorInterrupter.interrupt(contexts); + } + + if (status.messages.length > 0) { + if (status.exception) { + this.notificationService.logException( + status.exception, + status.messages[0], + status.messages.slice(1).join('\n') + ); + } else { + this.notificationService.notify({ + title: status.messages[0], + message: status.messages.slice(1).join('\n'), + }, status.saved ? ENotificationType.Success : ENotificationType.Error); + } + } + }; + + private ensureValidation: IExecutorHandler = (data, contexts) => { + const validation = contexts.getContext(this.configurationValidationContext); + + if (!validation.valid) { + ExecutorInterrupter.interrupt(contexts); + } + + if (validation.messages.length > 0) { + this.notificationService.notify({ + title: data.state.mode === 'edit' + ? 'administration_identity_providers_provider_save_error' + : 'administration_identity_providers_provider_create_error', + message: validation.messages.join('\n'), + }, validation.valid ? ENotificationType.Info : ENotificationType.Error); + } + }; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormState.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormState.ts new file mode 100644 index 0000000000..10ac03a37f --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RoleFormState.ts @@ -0,0 +1,160 @@ +/* + * 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 { computed, makeObservable, observable } from 'mobx'; + +import type { RoleInfo } from '@cloudbeaver/core-authentication'; +import { Executor, IExecutionContextProvider, IExecutor } from '@cloudbeaver/core-executor'; +import type { CachedMapResource } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; + +import { roleFormConfigureContext } from './Contexts/roleFormConfigureContext'; +import { IRoleFormStateInfo, roleFormStateContext } from './Contexts/roleFormStateContext'; +import type { IRoleFormState, IRoleFormSubmitData, RoleFormMode } from './IRoleFormProps'; +import type { RoleFormService } from './RoleFormService'; + +export class RoleFormState implements IRoleFormState { + mode: RoleFormMode; + config: RoleInfo; + statusMessage: string | null; + configured: boolean; + partsState: MetadataMap; + + get info(): RoleInfo | undefined { + if (!this.config.roleId) { + return undefined; + } + return this.resource.get(this.config.roleId); + } + + get loading(): boolean { + return this.loadRoleTask.executing || this.submittingTask.executing; + } + + get disabled(): boolean { + return this.loading || !!this.stateInfo?.disabled || this.loadRoleTask.executing; + } + + get readonly(): boolean { + return false; + } + + readonly resource: CachedMapResource; + + readonly service: RoleFormService; + readonly submittingTask: IExecutor; + + private stateInfo: IRoleFormStateInfo | null; + private loadRoleTask: IExecutor; + private formStateTask: IExecutor; + + constructor( + service: RoleFormService, + resource: CachedMapResource + ) { + this.resource = resource; + this.config = { + roleId: '', + }; + + this.stateInfo = null; + this.service = service; + this.formStateTask = new Executor(this, () => true); + this.loadRoleTask = new Executor(this, () => true); + this.submittingTask = new Executor(); + this.statusMessage = null; + this.configured = false; + this.partsState = new MetadataMap(); + this.mode = 'create'; + + makeObservable(this, { + mode: observable, + config: observable, + statusMessage: observable, + info: computed, + readonly: computed, + }); + + this.save = this.save.bind(this); + this.loadInfo = this.loadInfo.bind(this); + this.updateFormState = this.updateFormState.bind(this); + + this.formStateTask + .addCollection(service.formStateTask) + .addPostHandler(this.updateFormState); + + this.loadRoleTask + .before(service.configureTask) + .addPostHandler(this.loadInfo) + .next(service.fillConfigTask, (state, contexts) => { + const configuration = contexts.getContext(roleFormConfigureContext); + + return { + state, + updated: state.info !== configuration.info || !this.configured, + }; + }) + .next(this.formStateTask); + } + + async load(): Promise { } + + async loadRoleInfo(): Promise { + await this.loadRoleTask.execute(this); + + return this.info; + } + + setOptions( + mode: RoleFormMode, + ): this { + this.mode = mode; + return this; + } + + setConfig(config: RoleInfo): this { + this.config = config; + return this; + } + + async save(): Promise { + await this.submittingTask.executeScope( + { + state: this, + }, + this.service.formSubmittingTask + ); + } + + private updateFormState( + data: IRoleFormState, + contexts: IExecutionContextProvider + ): void { + const context = contexts.getContext(roleFormStateContext); + + this.statusMessage = context.statusMessage; + + this.stateInfo = context; + this.configured = true; + } + + private async loadInfo( + data: IRoleFormState, + contexts: IExecutionContextProvider + ) { + if (!data.config.roleId) { + return; + } + + if (!this.resource.has(data.config.roleId)) { + return; + } + + await this.resource.load(data.config.roleId); + } +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministration.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministration.tsx new file mode 100644 index 0000000000..5a8e2127d0 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministration.tsx @@ -0,0 +1,106 @@ +/* + * 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, 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 { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +import { CreateRole } from './CreateRole'; +import { CreateRoleService } from './CreateRoleService'; +import { RolesTable } from './RolesTable/RolesTable'; +import { useRolesTable } from './RolesTable/useRolesTable'; + +const styles = composes( + css` + layout-grid-cell { + composes: theme-background-surface theme-text-on-surface from global; + } + layout-grid-cell { + composes: theme-border-color-background from global; + } +`, + css` + layout-grid { + width: 100%; + overflow: auto; + } + layout-grid-inner { + min-height: 100%; + } + layout-grid-cell { + position: relative; + border: solid 1px; + } +`); + +export const RolesAdministration: AdministrationItemContentComponent = observer(function RolesAdministration({ + sub, +}) { + const translate = useTranslate(); + const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES, BASE_CONTAINERS_STYLES); + const service = useService(CreateRoleService); + + const tableState = useTable(); + const rolesTableState = useRolesTable(tableState); + + return styled(style)( + <> + + + {translate('ui_add')} + + + {translate('ui_refresh')} + + + {translate('ui_delete')} + + + + + {sub && ( + + + + )} + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministrationNavService.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministrationNavService.ts new file mode 100644 index 0000000000..82e6c945c6 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministrationNavService.ts @@ -0,0 +1,25 @@ +/* + * 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 { AdministrationScreenService } from '@cloudbeaver/core-administration'; +import { injectable } from '@cloudbeaver/core-di'; + +@injectable() +export class RolesAdministrationNavService { + constructor( + private administrationScreenService: AdministrationScreenService + ) { } + + navToRoot(): void { + this.administrationScreenService.navigateToItem('roles'); + } + + navToCreate(): void { + this.administrationScreenService.navigateToItemSub('roles', 'create'); + } +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministrationService.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministrationService.ts new file mode 100644 index 0000000000..bfffbd35a0 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesAdministrationService.ts @@ -0,0 +1,70 @@ +/* + * 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 { AdministrationItemService, AdministrationItemType } from '@cloudbeaver/core-administration'; +import { RoleInfo, RolesResource } from '@cloudbeaver/core-authentication'; +import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; +import { injectable, Bootstrap } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; + +import { CreateRoleService } from './CreateRoleService'; +import { RolesAdministration } from './RolesAdministration'; +import { RolesDrawerItem } from './RolesDrawerItem'; + +export interface IRoleDetailsInfoProps { + role: RoleInfo; +} + +@injectable() +export class RolesAdministrationService extends Bootstrap { + readonly roleDetailsInfoPlaceholder = new PlaceholderContainer(); + + constructor( + private readonly administrationItemService: AdministrationItemService, + private readonly createRoleService: CreateRoleService, + private readonly rolesResource: RolesResource, + private readonly notificationService: NotificationService, + ) { + super(); + } + + register(): void { + this.administrationItemService.create({ + name: 'roles', + type: AdministrationItemType.Default, + order: 3.1, + configurationWizardOptions: { + description: 'administration_roles_tab_description', + }, + sub: [ + { + name: 'create', + onActivate: () => this.createRoleService.fillData(), + }, + ], + getContentComponent: () => RolesAdministration, + getDrawerComponent: () => RolesDrawerItem, + onActivate: this.loadRoles.bind(this), + onDeActivate: (configurationWizard, outside) => { + if (outside) { + this.rolesResource.cleanNewFlags(); + } + }, + }); + } + + load(): void | Promise { } + + private async loadRoles() { + try { + await this.rolesResource.loadAll(); + } catch (exception) { + this.notificationService.logException(exception, 'Error occurred while loading roles'); + } + } +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesDrawerItem.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesDrawerItem.tsx new file mode 100644 index 0000000000..e84cd63921 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesDrawerItem.tsx @@ -0,0 +1,33 @@ +/* + * 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 styled from 'reshadow'; + +import type { AdministrationItemDrawerProps } from '@cloudbeaver/core-administration'; +import { Tab, TabTitle, TabIcon } from '@cloudbeaver/core-blocks'; +import { Translate } from '@cloudbeaver/core-localization'; +import { useStyles } from '@cloudbeaver/core-theming'; + +export const RolesDrawerItem: React.FC = function RolesDrawerItem({ + item, + onSelect, + style, + disabled, +}) { + return styled(useStyles(style))( + onSelect(item.name)} + > + + + + ); +}; diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/Role.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/Role.tsx new file mode 100644 index 0000000000..4433be8b63 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/Role.tsx @@ -0,0 +1,60 @@ +/* + * 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, use } from 'reshadow'; + +import type { RoleInfo } from '@cloudbeaver/core-authentication'; +import { TableItem, TableColumnValue, TableItemSelect, TableItemExpand, Placeholder } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import { RolesAdministrationService } from '../RolesAdministrationService'; +import { RoleEdit } from './RoleEdit'; + +const styles = css` + StaticImage { + display: flex; + width: 24px; + + &:not(:last-child) { + margin-right: 16px; + } + } + TableColumnValue[expand] { + cursor: pointer; + } + TableColumnValue[|gap] { + gap: 16px; + } +`; + +interface Props { + role: RoleInfo; +} + +export const Role = observer(function Role({ role }) { + const service = useService(RolesAdministrationService); + + return styled(useStyles(styles))( + + + + + + + + {role.roleId} + {role.roleName || ''} + {role.description || ''} + + + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/RoleEdit.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/RoleEdit.tsx new file mode 100644 index 0000000000..4a5d391581 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/RoleEdit.tsx @@ -0,0 +1,68 @@ +/* + * 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 { useRef, useEffect } from 'react'; +import styled, { css } from 'reshadow'; + +import { RolesResource } from '@cloudbeaver/core-authentication'; +import { useService } from '@cloudbeaver/core-di'; +import { useStyles, composes } from '@cloudbeaver/core-theming'; + +import { RoleForm } from '../RoleForm'; +import { useRoleFormState } from '../useRoleFormState'; + +const styles = composes( + css` + box { + composes: theme-background-secondary theme-text-on-secondary from global; + } + `, + css` + box { + box-sizing: border-box; + padding-bottom: 24px; + display: flex; + flex-direction: column; + height: 664px; + } + ` +); + +interface Props { + item: string; +} + +export const RoleEdit = observer(function RoleEdit({ + item, +}) { + const resource = useService(RolesResource); + const boxRef = useRef(null); + + useEffect(() => { + boxRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + }, []); + + const data = useRoleFormState( + resource, + state => state.setOptions('edit') + ); + + data.config.roleId = item; + + return styled(useStyles(styles))( + + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/RolesTable.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/RolesTable.tsx new file mode 100644 index 0000000000..ad8d4fa0ed --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/RolesTable.tsx @@ -0,0 +1,50 @@ +/* + * 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, use } from 'reshadow'; + +import type { RoleInfo } from '@cloudbeaver/core-authentication'; +import { Table, TableHeader, TableColumnHeader, TableBody } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import { Role } from './Role'; + +const styles = css` + Table { + width: 100%; + } +`; + +interface Props { + roles: RoleInfo[]; + selectedItems: Map; + expandedItems: Map; +} + +export const RolesTable = observer(function RolesTable({ roles, selectedItems, expandedItems }) { + const translate = useTranslate(); + return styled(useStyles(styles))( + + + + + {translate('administration_roles_role_id')} + {translate('administration_roles_role_name')} + {translate('administration_roles_role_description')} + + + + {roles.map(role => ( + + ))} + +
+ ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/useRolesTable.tsx b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/useRolesTable.tsx new file mode 100644 index 0000000000..8097292223 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/RolesTable/useRolesTable.tsx @@ -0,0 +1,89 @@ +/* + * 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 { computed, observable } from 'mobx'; + +import { compareRoles, RoleInfo, RolesResource } from '@cloudbeaver/core-authentication'; +import { ITableState, 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 { + processing: boolean; + tableState: ITableState; + roles: RoleInfo[]; + update: () => Promise; + delete: () => Promise; +} + +export function useRolesTable(tableState: ITableState): State { + const notificationService = useService(NotificationService); + const dialogService = useService(CommonDialogService); + const resource = useService(RolesResource); + + return useObservableRef(() => ({ + processing: false, + tableState, + get roles() { + return resource.values.slice().sort(compareRoles); + }, + async update() { + if (this.processing) { + return; + } + + try { + this.processing = true; + await resource.refreshAll(); + notificationService.logSuccess({ title: 'administration_roles_role_list_update_success' }); + } catch (exception) { + notificationService.logException(exception, 'administration_roles_role_list_update_fail'); + } finally { + this.processing = false; + } + }, + async delete() { + if (this.processing) { + return; + } + + const deletionList = this.tableState.selectedList; + + if (deletionList.length === 0) { + return; + } + + const result = await dialogService.open(ConfirmationDialog, { + title: 'ui_data_delete_confirmation', + message: `You're going to delete these roles: ${deletionList.map(name => `"${name}"`).join(', ')}. Are you sure?`, + confirmActionText: 'ui_delete', + }); + + if (result === DialogueStateResult.Rejected) { + return; + } + + try { + this.processing = true; + await resource.deleteRole(resourceKeyList(deletionList)); + + this.tableState.unselect(); + this.tableState.unexpand(deletionList); + } catch (exception) { + notificationService.logException(exception, 'Roles delete Error'); + } finally { + this.processing = false; + } + }, + }), { + processing: observable.ref, + roles: computed, + }, { tableState }, ['update', 'delete']); +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Roles/useRoleFormState.ts b/webapp/packages/plugin-authentication/src/Administration/Roles/useRoleFormState.ts new file mode 100644 index 0000000000..1880b8cbdb --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Roles/useRoleFormState.ts @@ -0,0 +1,36 @@ +/* + * 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 { useState } from 'react'; + +import type { RoleInfo } from '@cloudbeaver/core-authentication'; +import { useService } from '@cloudbeaver/core-di'; +import type { CachedMapResource } from '@cloudbeaver/core-sdk'; + +import type { IRoleFormState } from './IRoleFormProps'; +import { RoleFormService } from './RoleFormService'; +import { RoleFormState } from './RoleFormState'; + +export function useRoleFormState( + resource: CachedMapResource, + configure?: (state: IRoleFormState) => any +): IRoleFormState { + const service = useService(RoleFormService); + const [state] = useState(() => { + const state = new RoleFormState( + service, + resource, + ); + configure?.(state); + + state.load(); + return state; + }); + + return state; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormController.ts index c1518062d4..148310318e 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormController.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormController.ts @@ -8,7 +8,7 @@ import { observable, computed, makeObservable } from 'mobx'; -import { isLocalUser, RolesResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { isLocalUser, RoleInfo, RolesResource, UsersResource } from '@cloudbeaver/core-authentication'; import { DatabaseConnection, ConnectionsResource, DBDriverResource } from '@cloudbeaver/core-connections'; import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; @@ -16,7 +16,7 @@ import { ENotificationType, NotificationService } from '@cloudbeaver/core-events import type { TLocalizationToken } from '@cloudbeaver/core-localization'; import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; import { - GQLErrorCatcher, AdminConnectionGrantInfo, AdminSubjectType, AdminUserInfo, AdminRoleInfo + GQLErrorCatcher, AdminConnectionGrantInfo, AdminSubjectType, AdminUserInfo } from '@cloudbeaver/core-sdk'; interface IStatusMessage { @@ -45,7 +45,7 @@ export class UserFormController implements IInitializableController, IDestructib return Array.from(this.connectionsResource.data.values()); } - get roles(): AdminRoleInfo[] { + get roles(): RoleInfo[] { return Array.from(this.rolesResource.data.values()); } diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index 462748f2e7..1d1b0e24ed 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -72,4 +72,28 @@ export default [ ['administration_identity_providers_provider_create_error', 'Create configuration error'], ['administration_identity_providers_configuration_list_update_success', 'Configuration list was refreshed'], ['administration_identity_providers_configuration_list_update_fail', 'Configuration list refresh failed'], + + ['administration_roles_tab_title', 'Roles'], + ['administration_roles_tab_description', 'Role management'], + ['administration_roles_role_creation', 'Role creation'], + ['administration_roles_role_id', 'Role ID'], + ['administration_roles_role_name', 'Role name'], + ['administration_roles_role_description', 'Description'], + ['administration_roles_role_create_error', 'Create role error'], + ['administration_roles_role_save_error', 'Save role error'], + ['administration_roles_role_list_update_success', 'Role list was refreshed'], + ['administration_roles_role_list_update_fail', 'Role list refresh failed'], + ['administration_roles_add_tooltip', 'Add new role'], + ['administration_roles_refresh_tooltip', 'Refresh role list'], + ['administration_roles_delete_tooltip', 'Delete selected roles'], + + ['administration_roles_role_granted_users_tab_title', 'Granted users'], + ['administration_roles_role_granted_users_search_placeholder', 'Search for user ID...'], + ['administration_roles_role_granted_users_user_id', 'User ID'], + ['administration_roles_role_granted_users_user_name', 'User name'], + ['administration_roles_role_granted_users_empty', 'No available users'], + + ['administration_roles_role_granted_connections_tab_title', 'Granted connections'], + ['administration_roles_role_granted_connections_search_placeholder', 'Search for connection name...'], + ['administration_roles_role_granted_connections_empty', 'No available connections'], ]; diff --git a/webapp/packages/plugin-authentication/src/locales/it.ts b/webapp/packages/plugin-authentication/src/locales/it.ts index 0654c9fc3d..fa3694793a 100644 --- a/webapp/packages/plugin-authentication/src/locales/it.ts +++ b/webapp/packages/plugin-authentication/src/locales/it.ts @@ -35,7 +35,7 @@ export default [ ['authentication_user_login_not_set', 'Login non può esser vuoto'], ['authentication_user_role_not_set', 'Selezionare almeno un ruolo'], ['authentication_user_password_not_set', 'La Password è obbligatoria'], - ['authentication_user_passwords_not_match', "Le Passwords non coincidono"], + ['authentication_user_passwords_not_match', 'Le Passwords non coincidono'], ['authentication_user_login_already_exists', 'Esiste già un utente con quel nome'], ['authentication_user_login_cant_be_used', 'Quel nome non può essere usato'], ['authentication_user_password_change_dialog_title', 'Modifica la password (locale)'], diff --git a/webapp/packages/plugin-authentication/src/locales/ru.ts b/webapp/packages/plugin-authentication/src/locales/ru.ts index bc3924b4b9..c76af74021 100644 --- a/webapp/packages/plugin-authentication/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication/src/locales/ru.ts @@ -61,4 +61,28 @@ export default [ ['administration_identity_providers_provider_create_error', 'Ошибка при создании конфигурации'], ['administration_identity_providers_configuration_list_update_success', 'Список конфигураций успешно обновлен'], ['administration_identity_providers_configuration_list_update_fail', 'Не удалось обновить список конфигураций'], + + ['administration_roles_tab_title', 'Роли'], + ['administration_roles_tab_description', 'Управление ролями'], + ['administration_roles_role_creation', 'Создание роли'], + ['administration_roles_role_id', 'ID Роли'], + ['administration_roles_role_name', 'Название роли'], + ['administration_roles_role_description', 'Описание'], + ['administration_roles_role_create_error', 'Не удалось создать роль'], + ['administration_roles_role_save_error', 'Не удалось обновить роль'], + ['administration_roles_role_list_update_success', 'Список ролей обновлен'], + ['administration_roles_role_list_update_fail', 'Не удалось обновить список ролей'], + ['administration_roles_add_tooltip', 'Создать новую роль'], + ['administration_roles_refresh_tooltip', 'Обновить список ролей'], + ['administration_roles_delete_tooltip', 'Уадлить выбранные роли'], + + ['administration_roles_role_granted_users_tab_title', 'Связанные пользователи'], + ['administration_roles_role_granted_users_search_placeholder', 'Поиск по ID пользователя...'], + ['administration_roles_role_granted_users_user_id', 'ID пользователя'], + ['administration_roles_role_granted_users_user_name', 'Имя пользователя'], + ['administration_roles_role_granted_users_empty', 'Нет доступных пользователей'], + + ['administration_roles_role_granted_connections_tab_title', 'Связанные подключения'], + ['administration_roles_role_granted_connections_search_placeholder', 'Поиск по названию подключения...'], + ['administration_roles_role_granted_connections_empty', 'Нет доступных подключений'], ]; diff --git a/webapp/packages/plugin-authentication/src/manifest.ts b/webapp/packages/plugin-authentication/src/manifest.ts index c9ee3d9e8a..38d529743f 100644 --- a/webapp/packages/plugin-authentication/src/manifest.ts +++ b/webapp/packages/plugin-authentication/src/manifest.ts @@ -13,6 +13,13 @@ import { AuthConfigurationsAdministrationNavService } from './Administration/Ide import { AuthConfigurationsAdministrationService } from './Administration/IdentityProviders/AuthConfigurationsAdministrationService'; import { CreateAuthConfigurationService } from './Administration/IdentityProviders/CreateAuthConfigurationService'; import { AuthConfigurationOptionsTabService } from './Administration/IdentityProviders/Options/AuthConfigurationOptionsTabService'; +import { CreateRoleService } from './Administration/Roles/CreateRoleService'; +import { GrantedConnectionsTabService } from './Administration/Roles/GrantedConnections/GrantedConnectionsTabService'; +import { GrantedUsersTabService } from './Administration/Roles/GrantedUsers/GrantedUsersTabService'; +import { RoleOptionsTabService } from './Administration/Roles/Options/RoleOptionsTabService'; +import { RoleFormService } from './Administration/Roles/RoleFormService'; +import { RolesAdministrationNavService } from './Administration/Roles/RolesAdministrationNavService'; +import { RolesAdministrationService } from './Administration/Roles/RolesAdministrationService'; import { ServerConfigurationAuthenticationBootstrap } from './Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap'; import { CreateUserService } from './Administration/Users/CreateUserService'; import { UserFormBaseBootstrap } from './Administration/Users/UserForm/UserFormBaseBootstrap'; @@ -47,5 +54,12 @@ export const manifest: PluginManifest = { AuthConfigurationsAdministrationNavService, AuthConfigurationFormService, AuthConfigurationOptionsTabService, + RolesAdministrationService, + CreateRoleService, + RolesAdministrationNavService, + RoleFormService, + RoleOptionsTabService, + GrantedUsersTabService, + GrantedConnectionsTabService, ], };