Merge pull request #462 from dbeaver/feat/roles

Feat/roles
This commit is contained in:
Alexey
2021-09-02 20:28:33 +03:00
committed by GitHub
73 changed files with 3509 additions and 60 deletions
@@ -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<string, AdminRoleInfo> {
export class RolesResource extends CachedMapResource<string, RoleInfo> {
static keyAll = resourceKeyList(['all'], 'all');
private loadedKeyMetadata: MetadataMap<string, boolean>;
constructor(private graphQLService: GraphQLService) {
super();
@@ -31,30 +42,113 @@ export class RolesResource extends CachedMapResource<string, AdminRoleInfo> {
return this.data.has(id);
}
async loadAll() {
await this.load('all');
async loadAll(): Promise<Map<string, RoleInfo>> {
await this.load(RolesResource.keyAll);
return this.data;
}
async refreshAll() {
await this.refresh('all');
async refreshAll(): Promise<Map<string, RoleInfo>> {
await this.refresh(RolesResource.keyAll);
return this.data;
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, AdminRoleInfo>> {
const roleId = key === 'all' ? undefined : key as string;
async createRole(roleInfo: RoleInfo): Promise<RoleInfo> {
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<RoleInfo> {
const { role } = await this.graphQLService.sdk.updateRole(roleInfo);
this.updateRoles(role);
return this.get(roleInfo.roleId)!;
}
async deleteRole(key: ResourceKey<string>): Promise<Map<string, RoleInfo>> {
await ResourceKeyUtils.forEachAsync(key, async key => {
await this.graphQLService.sdk.deleteRole({
roleId: key,
});
this.delete(key);
});
return this.data;
}
async loadGrantedUsers(roleId: string): Promise<string[]> {
const { role } = await this.graphQLService.sdk.getRoleGrantedUsers({ roleId });
return role[0].grantedUsers;
}
async getSubjectConnectionAccess(subjectId: string): Promise<AdminConnectionGrantInfo[]> {
const { grantInfo } = await this.graphQLService.sdk.getSubjectConnectionAccess({ subjectId });
return grantInfo;
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, RoleInfo>> {
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<string> {
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);
}
@@ -182,6 +182,10 @@ export class UsersResource extends CachedMapResource<string, AdminUser, UserReso
this.loadedKeyMetadata.set(UsersResource.keyAll, false);
}
isActiveUser(userId: string): boolean {
return this.authInfoService.userInfo?.userId === userId;
}
protected async loader(key: ResourceKey<string>, includes: string[] | undefined): Promise<Map<string, AdminUser>> {
await ResourceKeyUtils.forEachAsync(key, async key => {
const { users } = await this.graphQLService.sdk.getUsersList({
@@ -206,10 +210,6 @@ export class UsersResource extends CachedMapResource<string, AdminUser, UserReso
return this.data;
}
private isActiveUser(userId: string) {
return this.authInfoService.userInfo?.userId === userId;
}
private getDefaultIncludes(): UserResourceIncludes {
return {
customIncludeOriginDetails: false,
@@ -107,7 +107,7 @@ export const Filter = observer<ControlledProps | ObjectsProps<any, any>>(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;
@@ -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<Props>(function TableColumnValue({
align,
children,
centerContent,
ellipsis,
flex,
expand,
className,
@@ -74,7 +76,7 @@ export const TableColumnValue = observer<Props>(function TableColumnValue({
<td
align={align}
className={className}
{...use({ centerContent })}
{...use({ centerContent, ellipsis })}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
{...rest}
@@ -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 { TLocalizationToken, 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 {
grantedUsers: AdminUserInfoFragment[];
grantedRoles: AdminRoleInfo[];
grantedRoles: RoleInfo[];
disabled: boolean;
onRevoke: (subjectIds: string[]) => void;
onEdit: () => void;
@@ -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;
@@ -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 ?? ''));
@@ -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.'],
@@ -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 минут. Чтобы продолжить работать, закройте этот диалог.'],
@@ -0,0 +1,5 @@
query createRole($roleId: ID!, $roleName: String, $description: String ) {
role: createRole(roleId: $roleId, roleName: $roleName, description: $description) {
...AdminRoleInfo
}
}
@@ -0,0 +1,3 @@
query deleteRole($roleId: ID!) {
deleteRole(roleId: $roleId)
}
@@ -0,0 +1,5 @@
query getRoleGrantedUsers($roleId: ID!) {
role: listRoles(roleId: $roleId) {
grantedUsers
}
}
@@ -1,8 +1,5 @@
query getRolesList($roleId: ID) {
roles: listRoles(roleId: $roleId) {
roleId
roleName
description
#rolePermissions
...AdminRoleInfo
}
}
@@ -0,0 +1,5 @@
query updateRole($roleId: ID!, $roleName: String, $description: String ) {
role: updateRole(roleId: $roleId, roleName: $roleName, description: $description) {
...AdminRoleInfo
}
}
@@ -0,0 +1,7 @@
query getSubjectConnectionAccess($subjectId: ID) {
grantInfo: getSubjectConnectionAccess(subjectId: $subjectId) {
connectionId,
subjectId,
subjectType,
}
}
@@ -0,0 +1,3 @@
query setSubjectConnectionAccess($subjectId: ID!, $connections: [ID!]!) {
setSubjectConnectionAccess(subjectId: $subjectId, connections: $connections)
}
@@ -0,0 +1,5 @@
fragment AdminRoleInfo on AdminRoleInfo {
roleId
roleName
description
}
+134 -18
View File
@@ -52,6 +52,8 @@ export interface AdminRoleInfo {
roleId: Scalars['ID'];
roleName?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
grantedUsers: Array<Scalars['ID']>;
grantedConnections: AdminConnectionGrantInfo[];
rolePermissions: Array<Maybe<Scalars['ID']>>;
}
@@ -1110,6 +1112,40 @@ export type AsyncTaskCancelMutationVariables = Exact<{
export interface AsyncTaskCancelMutation { result: Mutation['asyncTaskCancel'] }
export type CreateRoleQueryVariables = Exact<{
roleId: Scalars['ID'];
roleName?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
}>;
export interface CreateRoleQuery { role: AdminRoleInfoFragment }
export type DeleteRoleQueryVariables = Exact<{
roleId: Scalars['ID'];
}>;
export type DeleteRoleQuery = Pick<Query, 'deleteRole'>;
export type GetRoleGrantedUsersQueryVariables = Exact<{
roleId: Scalars['ID'];
}>;
export interface GetRoleGrantedUsersQuery { role: Array<Maybe<Pick<AdminRoleInfo, 'grantedUsers'>>> }
export type GetRolesListQueryVariables = Exact<{
roleId?: Maybe<Scalars['ID']>;
}>;
export interface GetRolesListQuery { roles: Array<Maybe<AdminRoleInfoFragment>> }
export type UpdateRoleQueryVariables = Exact<{
roleId: Scalars['ID'];
roleName?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
}>;
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<Maybe<Pick<AdminPermissionInfo, 'id' | 'label' | 'description' | 'provider' | 'category'>>> }
export type GetRolesListQueryVariables = Exact<{
roleId?: Maybe<Scalars['ID']>;
}>;
export interface GetRolesListQuery { roles: Array<Maybe<Pick<AdminRoleInfo, 'roleId' | 'roleName' | 'description'>>> }
export type GetUserGrantedConnectionsQueryVariables = Exact<{
userId?: Maybe<Scalars['ID']>;
}>;
@@ -1290,6 +1320,12 @@ export type GetConnectionsQueryVariables = Exact<{
export interface GetConnectionsQuery { connections: DatabaseConnectionFragment[] }
export type GetSubjectConnectionAccessQueryVariables = Exact<{
subjectId?: Maybe<Scalars['ID']>;
}>;
export interface GetSubjectConnectionAccessQuery { grantInfo: Array<Pick<AdminConnectionGrantInfo, 'connectionId' | 'subjectId' | 'subjectType'>> }
export type SearchDatabasesQueryVariables = Exact<{
hosts: Array<Scalars['String']> | Scalars['String'];
}>;
@@ -1303,6 +1339,13 @@ export type SetConnectionAccessQueryVariables = Exact<{
export type SetConnectionAccessQuery = Pick<Query, 'setConnectionSubjectAccess'>;
export type SetSubjectConnectionAccessQueryVariables = Exact<{
subjectId: Scalars['ID'];
connections: Array<Scalars['ID']> | Scalars['ID'];
}>;
export type SetSubjectConnectionAccessQuery = Pick<Query, 'setSubjectConnectionAccess'>;
export type UpdateConnectionConfigurationQueryVariables = Exact<{
id: Scalars['ID'];
config: ConnectionConfig;
@@ -1534,6 +1577,8 @@ export type NavGetStructContainersQueryVariables = Exact<{
export interface NavGetStructContainersQuery { navGetStructContainers: { catalogList: Array<Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>>; schemaList: Array<Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>> } }
export type AdminRoleInfoFragment = Pick<AdminRoleInfo, 'roleId' | 'roleName' | 'description'>;
export type AdminUserInfoFragment = (
Pick<AdminUserInfo, 'userId' | 'grantedRoles' | 'linkedAuthProviders'>
& { 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<AsyncTaskCancelMutation> {
return withWrapper(() => client.request<AsyncTaskCancelMutation>(AsyncTaskCancelDocument, variables));
},
createRole(variables: CreateRoleQueryVariables): Promise<CreateRoleQuery> {
return withWrapper(() => client.request<CreateRoleQuery>(CreateRoleDocument, variables));
},
deleteRole(variables: DeleteRoleQueryVariables): Promise<DeleteRoleQuery> {
return withWrapper(() => client.request<DeleteRoleQuery>(DeleteRoleDocument, variables));
},
getRoleGrantedUsers(variables: GetRoleGrantedUsersQueryVariables): Promise<GetRoleGrantedUsersQuery> {
return withWrapper(() => client.request<GetRoleGrantedUsersQuery>(GetRoleGrantedUsersDocument, variables));
},
getRolesList(variables?: GetRolesListQueryVariables): Promise<GetRolesListQuery> {
return withWrapper(() => client.request<GetRolesListQuery>(GetRolesListDocument, variables));
},
updateRole(variables: UpdateRoleQueryVariables): Promise<UpdateRoleQuery> {
return withWrapper(() => client.request<UpdateRoleQuery>(UpdateRoleDocument, variables));
},
authChangeLocalPassword(variables: AuthChangeLocalPasswordQueryVariables): Promise<AuthChangeLocalPasswordQuery> {
return withWrapper(() => client.request<AuthChangeLocalPasswordQuery>(AuthChangeLocalPasswordDocument, variables));
},
@@ -2998,9 +3111,6 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getPermissionsList(variables?: GetPermissionsListQueryVariables): Promise<GetPermissionsListQuery> {
return withWrapper(() => client.request<GetPermissionsListQuery>(GetPermissionsListDocument, variables));
},
getRolesList(variables?: GetRolesListQueryVariables): Promise<GetRolesListQuery> {
return withWrapper(() => client.request<GetRolesListQuery>(GetRolesListDocument, variables));
},
getUserGrantedConnections(variables?: GetUserGrantedConnectionsQueryVariables): Promise<GetUserGrantedConnectionsQuery> {
return withWrapper(() => client.request<GetUserGrantedConnectionsQuery>(GetUserGrantedConnectionsDocument, variables));
},
@@ -3034,12 +3144,18 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getConnections(variables: GetConnectionsQueryVariables): Promise<GetConnectionsQuery> {
return withWrapper(() => client.request<GetConnectionsQuery>(GetConnectionsDocument, variables));
},
getSubjectConnectionAccess(variables?: GetSubjectConnectionAccessQueryVariables): Promise<GetSubjectConnectionAccessQuery> {
return withWrapper(() => client.request<GetSubjectConnectionAccessQuery>(GetSubjectConnectionAccessDocument, variables));
},
searchDatabases(variables: SearchDatabasesQueryVariables): Promise<SearchDatabasesQuery> {
return withWrapper(() => client.request<SearchDatabasesQuery>(SearchDatabasesDocument, variables));
},
setConnectionAccess(variables: SetConnectionAccessQueryVariables): Promise<SetConnectionAccessQuery> {
return withWrapper(() => client.request<SetConnectionAccessQuery>(SetConnectionAccessDocument, variables));
},
setSubjectConnectionAccess(variables: SetSubjectConnectionAccessQueryVariables): Promise<SetSubjectConnectionAccessQuery> {
return withWrapper(() => client.request<SetSubjectConnectionAccessQuery>(SetSubjectConnectionAccessDocument, variables));
},
updateConnectionConfiguration(variables: UpdateConnectionConfigurationQueryVariables): Promise<UpdateConnectionConfigurationQuery> {
return withWrapper(() => client.request<UpdateConnectionConfigurationQuery>(UpdateConnectionConfigurationDocument, variables));
},
@@ -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;
+1
View File
@@ -25,3 +25,4 @@ export * from './isValidUrl';
export * from './isImageFormat';
export * from './getCookies';
export * from './getUniqueName';
export * from './isMapsEqual';
@@ -11,5 +11,5 @@ export function isArraysEqual<T>(first: T[], second: T[]): boolean {
return false;
}
return !first.some(region => !second.includes(region));
return !first.some(value => !second.includes(value));
}
@@ -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<any, any>, map2: Map<any, any>): 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;
}
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.62055 4.47056C2.62055 2.63689 4.10744 1.15 5.94111 1.15C7.77538 1.15 9.26168 2.637 9.26168 4.47056C9.26168 6.30413 7.77538 7.79112 5.94111 7.79112C4.10744 7.79112 2.62055 6.30423 2.62055 4.47056ZM5.94111 2.85C5.04632 2.85 4.32055 3.57577 4.32055 4.47056C4.32055 5.36535 5.04632 6.09112 5.94111 6.09112C6.83628 6.09112 7.56168 5.36546 7.56168 4.47056C7.56168 3.57566 6.83628 2.85 5.94111 2.85ZM10.049 1.30344C10.367 1.20471 10.7069 1.15 11.0589 1.15C12.8925 1.15 14.3794 2.63689 14.3794 4.47056C14.3794 6.30423 12.8925 7.79112 11.0589 7.79112C10.7069 7.79112 10.3577 7.73634 10.024 7.6213C9.77146 7.53422 9.57452 7.33346 9.49232 7.07926C9.41011 6.82506 9.4522 6.547 9.60596 6.32852C9.98421 5.79104 10.2089 5.15261 10.2089 4.47056C10.2089 3.78675 9.98311 3.14032 9.60596 2.60441C9.44953 2.38214 9.40884 2.09847 9.49646 1.84118C9.58408 1.5839 9.78946 1.38403 10.049 1.30344ZM11.6697 2.96914C11.8236 3.44201 11.9089 3.94676 11.9089 4.47056C11.9089 4.99649 11.8229 5.50079 11.6682 5.9726C12.2612 5.73171 12.6794 5.14984 12.6794 4.47056C12.6794 3.79186 12.2619 3.2104 11.6697 2.96914ZM2.53737 9.41669C3.66938 8.96392 5.01199 8.73832 5.94111 8.73832C6.87047 8.73832 8.21308 8.96392 9.34505 9.41669C9.91169 9.64334 10.4791 9.94783 10.9184 10.3534C11.3637 10.7644 11.7322 11.3353 11.7322 12.0589V13.7056C11.7322 14.175 11.3517 14.5556 10.8822 14.5556H0.999994C0.530552 14.5556 0.149994 14.175 0.149994 13.7056V12.0589C0.149994 11.3353 0.518625 10.7644 0.963951 10.3533C1.40331 9.94781 1.97071 9.64333 2.53737 9.41669ZM2.117 11.6025C1.89316 11.8091 1.84999 11.9589 1.84999 12.0589V12.8556H10.0322V12.0589C10.0322 11.9588 9.98905 11.8091 9.76526 11.6025C9.5355 11.3904 9.17644 11.1802 8.7137 10.9951C7.78691 10.6244 6.65896 10.4383 5.94111 10.4383C5.22352 10.4383 4.09557 10.6244 3.16869 10.9951C2.70592 11.1802 2.3468 11.3904 2.117 11.6025ZM11.6246 9.35691C11.7873 9.01241 12.1594 8.81827 12.5351 8.88184C13.4524 9.0371 14.2734 9.3484 14.8785 9.88053C15.5101 10.4359 15.85 11.1847 15.85 12.0589V13.7056C15.85 14.175 15.4694 14.5556 15 14.5556H13.5294C13.06 14.5556 12.6794 14.175 12.6794 13.7056V12.0589C12.6794 11.3856 12.3766 10.831 11.836 10.3618C11.5483 10.1121 11.4619 9.7014 11.6246 9.35691Z" fill="#EE8A18"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -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`
@@ -59,9 +59,9 @@ export const AuthConfiguration = observer<Props>(function AuthConfiguration({ co
<TableColumnValue centerContent flex expand>
<StaticImage icon={icon} title={`${configuration.displayName} icon`} />
</TableColumnValue>
<TableColumnValue expand>{configuration.displayName}</TableColumnValue>
<TableColumnValue title={configuration.displayName} expand ellipsis>{configuration.displayName}</TableColumnValue>
<TableColumnValue>{configuration.providerId}</TableColumnValue>
<TableColumnValue>{configuration.description || ''}</TableColumnValue>
<TableColumnValue title={configuration.description} ellipsis>{configuration.description || ''}</TableColumnValue>
<TableColumnValue>
<FieldCheckboxNew checked={configuration.disabled} disabled />
</TableColumnValue>
@@ -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: '',
};
}
@@ -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<IRoleFormState>,
state: IRoleFormState
): IRoleFormConfigureContext {
return {
info: state.info,
};
}
@@ -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;
},
};
}
@@ -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)(
<role-create>
<title-bar>
<Translate token='administration_roles_role_creation' />
<fill />
<IconButton name="cross" viewBox="0 0 16 16" onClick={service.cancelCreate} />
</title-bar>
<role-create-content>
<RoleForm state={service.data} onCancel={service.cancelCreate} onSave={service.cancelCreate} />
</role-create-content>
</role-create>
);
});
@@ -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();
}
}
@@ -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<Props>(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<Map<any, boolean>>(() => observable(new Map()));
const [filterState] = useState<IFilterState>(() => 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)(
<Group box medium overflow>
<container>
<GrantedConnectionsTableHeader filterState={filterState} disabled={disabled}>
<Button disabled={disabled || !selected} mod={['unelevated']} onClick={grant}>{translate('ui_grant')}</Button>
</GrantedConnectionsTableHeader>
<table-container>
<Table selectedItems={selectedSubjects}>
<GrantedConnectionsTableInnerHeader />
<TableBody>
{!connections.length && filterState.filterValue && (
<TableItem item='tableInfo' selectDisabled>
<TableColumnValue colSpan={5}>
{translate('connections_connection_access_filter_no_result')}
</TableColumnValue>
</TableItem>
)}
{connections.map(connection => {
const driver = driversResource.get(connection.driverId);
return (
<GrantedConnectionsTableItem
key={connection.id}
id={connection.id}
name={connection.name}
host={`${connection.host || ''}${connection.host && connection.port ? ':' + connection.port : ''}`}
icon={driver?.icon}
disabled={disabled || grantedSubjects.includes(connection.id)}
/>
);
})}
</TableBody>
</Table>
</table-container>
</container>
</Group>
);
});
@@ -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<IRoleFormProps> = 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)(
<Loader state={[connections, dbDriverResource, state]}>
{() => styled(style)(
<ColoredContainer parent gap vertical>
{!connections.resource.values.length ? (
<Group large>
<TextPlaceholder>{translate('administration_roles_role_granted_connections_empty')}</TextPlaceholder>
</Group>
) : (
<>
{infoItem && (
<info-item>
<IconOrImage icon={infoItem.icon} />
{translate(infoItem.text)}
</info-item>
)}
<Container gap overflow>
<GrantedConnectionList
grantedConnections={grantedConnections.get()}
disabled={formState.disabled}
onEdit={edit}
onRevoke={revoke}
/>
{state.editing && (
<ConnectionList
connectionList={connections.resource.values}
grantedSubjects={state.grantedSubjects}
disabled={formState.disabled}
onGrant={grant}
/>
)}
</Container>
</>
)}
</ColoredContainer>
)}
</Loader>
);
});
@@ -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<Props>(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<Map<any, boolean>>(() => observable(new Map()));
const [filterState] = useState<IFilterState>(() => 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)(
<Group box medium overflow>
<container>
<GrantedConnectionsTableHeader filterState={filterState} disabled={disabled}>
<Button disabled={disabled || !selected} mod={['outlined']} onClick={revoke}>{translate('ui_revoke')}</Button>
<Button disabled={disabled} mod={['unelevated']} onClick={props.onEdit}>{translate('ui_edit')}</Button>
</GrantedConnectionsTableHeader>
<table-container>
<Table selectedItems={selectedSubjects}>
<GrantedConnectionsTableInnerHeader />
<TableBody>
<TableItem item='tableInfo' selectDisabled>
{tableInfoText && (
<TableColumnValue colSpan={5}>
{translate(tableInfoText)}
</TableColumnValue>
)}
</TableItem>
{connections.map(connection => {
const driver = driversResource.get(connection.driverId);
return (
<GrantedConnectionsTableItem
key={connection.id}
id={connection.id}
name={connection.name}
host={`${connection.host || ''}${connection.port ? ':' + connection.port : ''}`}
icon={driver?.icon}
disabled={disabled}
/>
);
})}
</TableBody>
</Table>
</table-container>
</container>
</Group>
);
});
@@ -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> | void { }
private stateGetter(context: IRoleFormProps): MetadataValueGetter<string, IGrantedConnectionsTabState> {
return () => ({
loading: false,
loaded: false,
editing: false,
grantedSubjects: [],
initialGrantedSubjects: [],
});
}
private async save(
data: IRoleFormSubmitData,
contexts: IExecutionContextProvider<IRoleFormSubmitData>
) {
const config = contexts.getContext(roleContext);
const status = contexts.getContext(this.roleFormService.configurationStatusContext);
if (!status.saved) {
return;
}
const state = this.roleFormService.tabsContainer.getTabState<IGrantedConnectionsTabState>(
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);
}
}
}
@@ -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<Props>(function GrantedConnectionsTableHeader({ filterState, disabled, className, children }) {
const translate = useTranslate();
return styled(useStyles(styles))(
<header className={className}>
<Filter
disabled={disabled}
placeholder={translate('administration_roles_role_granted_connections_search_placeholder')}
name='filterValue'
state={filterState}
/>
<buttons>
{children}
</buttons>
</header>
);
});
@@ -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<Props>(function GrantedConnectionsTableInnerHeader({ className }) {
const translate = useTranslate();
return styled(useStyles(styles))(
<TableHeader className={className}>
<TableColumnHeader min />
<TableColumnHeader min />
<TableColumnHeader>{translate('connections_connection_name')}</TableColumnHeader>
<TableColumnHeader>{translate('connections_connection_address')}</TableColumnHeader>
</TableHeader>
);
});
@@ -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<Props>(function GrantedConnectionsTableItem({
id, name, host, icon, iconTooltip, disabled, className,
}) {
const translate = useTranslate();
return styled(style)(
<TableItem
item={id}
disabled={disabled}
selectDisabled={disabled}
className={className}
>
<TableColumnValue centerContent flex>
<TableItemSelect disabled={disabled} />
</TableColumnValue>
<TableColumnValue>{icon && <StaticImage icon={icon} title={translate(iconTooltip)} />}</TableColumnValue>
<TableColumnValue>{name}</TableColumnValue>
<TableColumnValue>{host && host}</TableColumnValue>
</TableItem>
);
});
@@ -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;
}
@@ -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));
}
@@ -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<void>;
}
export function useGrantedConnections(role: RoleInfo, mode: RoleFormMode): IConnectionAccessState {
const resource = useService(RolesResource);
const notificationService = useService(NotificationService);
const state = useTabState<IGrantedConnectionsTabState>();
const edit = () => {
state.editing = !state.editing;
};
const revoke = (subjectIds: string[]): void => {
state.grantedSubjects = state.grantedSubjects.filter(subject => !subjectIds.includes(subject));
};
const grant = (subjectIds: string[]): void => {
state.grantedSubjects.push(...subjectIds);
};
const load = async () => {
if (state.loaded || state.loading) {
return;
}
try {
state.loading = true;
if (mode === 'edit') {
const grantInfo = await resource.getSubjectConnectionAccess(role.roleId);
state.grantedSubjects = grantInfo.map(subject => subject.connectionId);
state.initialGrantedSubjects = state.grantedSubjects.slice();
}
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 });
}
@@ -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<Props>(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<Map<any, boolean>>(() => observable(new Map()));
const [filterState] = useState<IFilterState>(() => 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)(
<Group box medium overflow>
<container>
<GrantedUsersTableHeader filterState={filterState} disabled={disabled}>
<Button disabled={disabled || !selected} mod={['outlined']} onClick={revoke}>{translate('ui_revoke')}</Button>
<Button disabled={disabled} mod={['unelevated']} onClick={props.onEdit}>{translate('ui_edit')}</Button>
</GrantedUsersTableHeader>
<table-container>
<Table selectedItems={selectedSubjects}>
<GrantedUsersTableInnerHeader />
<TableBody>
<TableItem item='tableInfo' selectDisabled>
{tableInfoText && (
<TableColumnValue colSpan={5}>
{translate(tableInfoText)}
</TableColumnValue>
)}
</TableItem>
{users.map(user => (
<GrantedUsersTableItem
key={user.userId}
id={user.userId}
name={`${user.userId}${usersResource.isActiveUser(user.userId) ? ' (you)' : ''}`}
icon='/icons/user.svg'
iconTooltip={translate('connections_connection_access_user_tooltip')}
disabled={disabled}
/>
))}
</TableBody>
</Table>
</table-container>
</container>
</Group>
);
});
@@ -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<IRoleFormProps> = 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)(
<Loader state={[users, state]}>
{() => styled(style)(
<ColoredContainer parent gap vertical>
{!users.resource.values.length ? (
<Group keepSize large>
<TextPlaceholder>{translate('administration_roles_role_granted_users_empty')}</TextPlaceholder>
</Group>
) : (
<>
{infoItem && (
<info-item>
<IconOrImage icon={infoItem.icon} />
{translate(infoItem.text)}
</info-item>
)}
<Container gap overflow>
<GrantedUserList
grantedUsers={grantedUsers.get()}
disabled={formState.disabled}
onEdit={edit}
onRevoke={revoke}
/>
{state.editing && (
<UserList
userList={users.resource.values}
grantedUsers={state.grantedUsers}
disabled={formState.disabled}
onGrant={grant}
/>
)}
</Container>
</>
)}
</ColoredContainer>
)}
</Loader>
);
});
@@ -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<string, IGrantedUsersTabState> {
return () => ({
loading: false,
loaded: false,
editing: false,
grantedUsers: [],
initialGrantedUsers: [],
});
}
private async save(
data: IRoleFormSubmitData,
contexts: IExecutionContextProvider<IRoleFormSubmitData>
) {
const config = contexts.getContext(roleContext);
const status = contexts.getContext(this.roleFormService.configurationStatusContext);
if (!status.saved) {
return;
}
const state = this.roleFormService.tabsContainer.getTabState<IGrantedUsersTabState>(
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(', ')}"`);
}
}
}
@@ -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<Props>(function GrantedUsersTableHeader({ filterState, disabled, className, children }) {
const translate = useTranslate();
return styled(useStyles(styles))(
<header className={className}>
<Filter
disabled={disabled}
placeholder={translate('administration_roles_role_granted_users_search_placeholder')}
name='filterValue'
state={filterState}
/>
<buttons>
{children}
</buttons>
</header>
);
});
@@ -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<Props>(function GrantedUsersTableInnerHeader({ className }) {
const translate = useTranslate();
return styled(useStyles(styles))(
<TableHeader className={className}>
<TableColumnHeader min />
<TableColumnHeader min />
<TableColumnHeader>{translate('administration_roles_role_granted_users_user_id')}</TableColumnHeader>
</TableHeader>
);
});
@@ -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<Props>(function GrantedUsersTableItem({
id, name, icon, iconTooltip, disabled, className,
}) {
const translate = useTranslate();
return styled(style)(
<TableItem
item={id}
disabled={disabled}
selectDisabled={disabled}
className={className}
>
<TableColumnValue centerContent flex>
<TableItemSelect disabled={disabled} />
</TableColumnValue>
<TableColumnValue><StaticImage icon={icon} title={translate(iconTooltip)} /></TableColumnValue>
<TableColumnValue>{name}</TableColumnValue>
</TableItem>
);
});
@@ -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;
}
@@ -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<Props>(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<Map<any, boolean>>(() => observable(new Map()));
const [filterState] = useState<IFilterState>(() => 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)(
<Group box medium overflow>
<container>
<GrantedUsersTableHeader filterState={filterState} disabled={disabled}>
<Button disabled={disabled || !selected} mod={['unelevated']} onClick={grant}>{translate('ui_grant')}</Button>
</GrantedUsersTableHeader>
<table-container>
<Table selectedItems={selectedSubjects}>
<GrantedUsersTableInnerHeader />
<TableBody>
{!users.length && filterState.filterValue && (
<TableItem item='tableInfo' selectDisabled>
<TableColumnValue colSpan={5}>
{translate('connections_connection_access_filter_no_result')}
</TableColumnValue>
</TableItem>
)}
{users.map(user => (
<GrantedUsersTableItem
key={user.userId}
id={user.userId}
name={`${user.userId}${usersResource.isActiveUser(user.userId) ? ' (you)' : ''}`}
icon='/icons/user.svg'
iconTooltip={translate('connections_connection_access_user_tooltip')}
disabled={disabled || !!grantedUsers.includes(user.userId)}
/>
))}
</TableBody>
</Table>
</table-container>
</container>
</Group>
);
});
@@ -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));
}
@@ -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<void>;
}
export function useGrantedUsers(role: RoleInfo, mode: RoleFormMode): IConnectionAccessState {
const resource = useService(RolesResource);
const notificationService = useService(NotificationService);
const state = useTabState<IGrantedUsersTabState>();
const edit = () => {
state.editing = !state.editing;
};
const revoke = (subjectIds: string[]): void => {
state.grantedUsers = state.grantedUsers.filter(subject => !subjectIds.includes(subject));
};
const grant = (subjectIds: string[]): void => {
state.grantedUsers.push(...subjectIds);
};
const load = async () => {
if (state.loaded || state.loading) {
return;
}
try {
state.loading = true;
if (mode === 'edit') {
const grantedUsers = await resource.loadGrantedUsers(role.roleId);
state.grantedUsers = grantedUsers;
state.initialGrantedUsers = state.grantedUsers.slice();
}
state.loaded = true;
} catch (exception) {
notificationService.logException(exception, "Can't load users info");
}
state.loading = false;
};
return useObjectRef({ state, revoke, grant, edit, load });
}
@@ -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<string, any>;
readonly info: RoleInfo | undefined;
readonly statusMessage: string | null;
readonly disabled: boolean;
readonly readonly: boolean;
readonly loading: boolean;
readonly submittingTask: IExecutorHandlersCollection<IRoleFormSubmitData>;
readonly resource: CachedMapResource<string, RoleInfo>;
readonly load: () => Promise<void>;
readonly loadRoleInfo: () => Promise<RoleInfo | undefined>;
readonly save: () => Promise<void>;
readonly setOptions: (
mode: RoleFormMode,
) => this;
}
export interface IRoleFormProps {
state: IRoleFormState;
onCancel?: () => void;
}
export interface IRoleFormFillConfigData {
updated: boolean;
state: IRoleFormState;
}
export interface IRoleFormSubmitData {
state: IRoleFormState;
}
@@ -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<IRoleFormProps> = observer(function RoleOptions({
state,
}) {
const formRef = useRef<HTMLFormElement>(null);
const translate = useTranslate();
const style = useStyles(BASE_CONTAINERS_STYLES, styles);
const edit = state.mode === 'edit';
return styled(style)(
<SubmittingForm ref={formRef} onSubmit={state.save}>
<ColoredContainer parent gap overflow>
<Group small gap>
<InputFieldNew
name='roleId'
state={state.config}
readOnly={state.readonly || edit}
disabled={state.disabled}
required
tiny
fill
>
{translate('administration_roles_role_id')}
</InputFieldNew>
<InputFieldNew
name='roleName'
state={state.config}
readOnly={state.readonly}
disabled={state.disabled}
tiny
fill
>
{translate('administration_roles_role_name')}
</InputFieldNew>
<TextareaNew
name='description'
state={state.config}
readOnly={state.readonly}
disabled={state.disabled}
tiny
fill
>
{translate('administration_roles_role_description')}
</TextareaNew>
</Group>
</ColoredContainer>
</SubmittingForm>
);
});
@@ -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<IRoleFormSubmitData>
) {
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<IRoleFormSubmitData>
) {
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<IRoleFormSubmitData>
) {
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<IRoleFormFillConfigData>
) {
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;
}
}
}
@@ -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<Props>(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)(
<TabsState
container={service.tabsContainer}
localState={state.partsState}
state={state}
onCancel={onCancel}
>
<box className={className}>
<role-top-bar>
<role-top-bar-tabs>
<role-status-message>
{state.statusMessage && (
<>
<IconOrImage icon='/icons/info_icon.svg' />
{translate(state.statusMessage)}
</>
)}
</role-status-message>
<TabList style={style} disabled={false} />
</role-top-bar-tabs>
<role-top-bar-actions>
<Placeholder container={service.actionsContainer} state={state} onCancel={onCancel} />
</role-top-bar-actions>
</role-top-bar>
<content-box>
<TabPanelList style={style} />
</content-box>
</box>
</TabsState>
);
});
@@ -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<IRoleFormProps> = observer(function RoleFormBaseActions({
state,
onCancel,
}) {
const translate = useTranslate();
return (
<>
{onCancel && (
<Button
type="button"
disabled={state.disabled}
mod={['outlined']}
onClick={onCancel}
>
{translate('ui_processing_cancel')}
</Button>
)}
<Button
type="button"
disabled={state.disabled || state.readonly}
mod={['unelevated']}
loader
onClick={state.save}
>
{translate(state.mode === 'edit' ? 'ui_processing_save' : 'ui_processing_create')}
</Button>
</>
);
});
@@ -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<IRoleFormProps>;
readonly actionsContainer: PlaceholderContainer<IRoleFormProps>;
readonly configureTask: IExecutorHandlersCollection<IRoleFormState>;
readonly fillConfigTask: IExecutorHandlersCollection<IRoleFormFillConfigData>;
readonly prepareConfigTask: IExecutorHandlersCollection<IRoleFormSubmitData>;
readonly formValidationTask: IExecutorHandlersCollection<IRoleFormSubmitData>;
readonly formSubmittingTask: IExecutorHandlersCollection<IRoleFormSubmitData>;
readonly formStateTask: IExecutorHandlersCollection<IRoleFormState>;
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<IRoleFormSubmitData>(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<IRoleFormSubmitData> = (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<IRoleFormSubmitData> = (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);
}
};
}
@@ -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<string, any>;
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<string, RoleInfo>;
readonly service: RoleFormService;
readonly submittingTask: IExecutor<IRoleFormSubmitData>;
private stateInfo: IRoleFormStateInfo | null;
private loadRoleTask: IExecutor<IRoleFormState>;
private formStateTask: IExecutor<IRoleFormState>;
constructor(
service: RoleFormService,
resource: CachedMapResource<string, RoleInfo>
) {
this.resource = resource;
this.config = {
roleId: '',
};
this.stateInfo = null;
this.service = service;
this.formStateTask = new Executor<IRoleFormState>(this, () => true);
this.loadRoleTask = new Executor<IRoleFormState>(this, () => true);
this.submittingTask = new Executor();
this.statusMessage = null;
this.configured = false;
this.partsState = new MetadataMap();
this.mode = 'create';
makeObservable<IRoleFormState>(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<void> { }
async loadRoleInfo(): Promise<RoleInfo | undefined> {
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<void> {
await this.submittingTask.executeScope(
{
state: this,
},
this.service.formSubmittingTask
);
}
private updateFormState(
data: IRoleFormState,
contexts: IExecutionContextProvider<IRoleFormState>
): void {
const context = contexts.getContext(roleFormStateContext);
this.statusMessage = context.statusMessage;
this.stateInfo = context;
this.configured = true;
}
private async loadInfo(
data: IRoleFormState,
contexts: IExecutionContextProvider<IRoleFormState>
) {
if (!data.config.roleId) {
return;
}
if (!this.resource.has(data.config.roleId)) {
return;
}
await this.resource.load(data.config.roleId);
}
}
@@ -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)(
<>
<ToolsPanel>
<ToolsAction
title={translate('administration_roles_add_tooltip')}
icon="add"
viewBox="0 0 24 24"
disabled={!!sub || rolesTableState.processing}
onClick={service.create}
>
{translate('ui_add')}
</ToolsAction>
<ToolsAction
title={translate('administration_roles_refresh_tooltip')}
icon="refresh"
viewBox="0 0 24 24"
disabled={rolesTableState.processing}
onClick={rolesTableState.update}
>
{translate('ui_refresh')}
</ToolsAction>
<ToolsAction
title={translate('administration_roles_delete_tooltip')}
icon="trash"
viewBox="0 0 24 24"
disabled={!tableState.itemsSelected || rolesTableState.processing}
onClick={rolesTableState.delete}
>
{translate('ui_delete')}
</ToolsAction>
</ToolsPanel>
<layout-grid>
<layout-grid-inner>
{sub && (
<layout-grid-cell {...use({ span: 12 })}>
<CreateRole />
</layout-grid-cell>
)}
<layout-grid-cell {...use({ span: 12 })}>
<RolesTable
roles={rolesTableState.roles}
selectedItems={tableState.selected}
expandedItems={tableState.expanded}
/>
<Loader loading={false} overlay />
</layout-grid-cell>
</layout-grid-inner>
</layout-grid>
</>
);
});
@@ -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');
}
}
@@ -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<IRoleDetailsInfoProps>();
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<void> { }
private async loadRoles() {
try {
await this.rolesResource.loadAll();
} catch (exception) {
this.notificationService.logException(exception, 'Error occurred while loading roles');
}
}
}
@@ -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<AdministrationItemDrawerProps> = function RolesDrawerItem({
item,
onSelect,
style,
disabled,
}) {
return styled(useStyles(style))(
<Tab
tabId={item.name}
disabled={disabled}
title='administration_roles_tab_title'
onOpen={() => onSelect(item.name)}
>
<TabIcon icon='/icons/tab_role.svg' />
<TabTitle><Translate token='administration_roles_tab_title' /></TabTitle>
</Tab>
);
};
@@ -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<Props>(function Role({ role }) {
const service = useService(RolesAdministrationService);
return styled(useStyles(styles))(
<TableItem item={role.roleId} expandElement={RoleEdit}>
<TableColumnValue centerContent flex>
<TableItemSelect />
</TableColumnValue>
<TableColumnValue centerContent flex expand>
<TableItemExpand />
</TableColumnValue>
<TableColumnValue expand>{role.roleId}</TableColumnValue>
<TableColumnValue>{role.roleName || ''}</TableColumnValue>
<TableColumnValue title={role.description} ellipsis>{role.description || ''}</TableColumnValue>
<TableColumnValue flex {...use({ gap: true })}>
<Placeholder container={service.roleDetailsInfoPlaceholder} role={role} />
</TableColumnValue>
</TableItem>
);
});
@@ -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<Props>(function RoleEdit({
item,
}) {
const resource = useService(RolesResource);
const boxRef = useRef<HTMLDivElement>(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))(
<box ref={boxRef} as='div'>
<RoleForm
state={data}
/>
</box>
);
});
@@ -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<string, boolean>;
expandedItems: Map<string, boolean>;
}
export const RolesTable = observer<Props>(function RolesTable({ roles, selectedItems, expandedItems }) {
const translate = useTranslate();
return styled(useStyles(styles))(
<Table selectedItems={selectedItems} expandedItems={expandedItems} {...use({ size: 'big' })}>
<TableHeader>
<TableColumnHeader min />
<TableColumnHeader min />
<TableColumnHeader>{translate('administration_roles_role_id')}</TableColumnHeader>
<TableColumnHeader>{translate('administration_roles_role_name')}</TableColumnHeader>
<TableColumnHeader>{translate('administration_roles_role_description')}</TableColumnHeader>
<TableColumnHeader />
</TableHeader>
<TableBody>
{roles.map(role => (
<Role key={role.roleId} role={role} />
))}
</TableBody>
</Table>
);
});
@@ -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<void>;
delete: () => Promise<void>;
}
export function useRolesTable(tableState: ITableState): State {
const notificationService = useService(NotificationService);
const dialogService = useService(CommonDialogService);
const resource = useService(RolesResource);
return useObservableRef<State>(() => ({
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']);
}
@@ -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<string, RoleInfo>,
configure?: (state: IRoleFormState) => any
): IRoleFormState {
const service = useService(RoleFormService);
const [state] = useState<IRoleFormState>(() => {
const state = new RoleFormState(
service,
resource,
);
configure?.(state);
state.load();
return state;
});
return state;
}
@@ -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());
}
@@ -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'],
];
@@ -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)'],
@@ -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', 'Нет доступных подключений'],
];
@@ -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,
],
};