mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
refactor: CB-1452 loader context
This commit is contained in:
@@ -15,9 +15,9 @@ import {
|
||||
ResourceKeyList,
|
||||
ResourceKeyUtils,
|
||||
AdminRoleInfoFragment,
|
||||
AdminConnectionGrantInfo
|
||||
AdminConnectionGrantInfo,
|
||||
CachedMapAllKey
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
const NEW_ROLE_SYMBOL = Symbol('new-role');
|
||||
|
||||
@@ -26,29 +26,17 @@ type NewRole = RoleInfo & { [NEW_ROLE_SYMBOL]: boolean; timestamp: number };
|
||||
|
||||
@injectable()
|
||||
export class RolesResource extends CachedMapResource<string, RoleInfo> {
|
||||
static keyAll = resourceKeyList(['all'], 'all');
|
||||
|
||||
private loadedKeyMetadata: MetadataMap<string, boolean>;
|
||||
constructor(private graphQLService: GraphQLService) {
|
||||
super();
|
||||
this.loadedKeyMetadata = new MetadataMap(() => false);
|
||||
}
|
||||
|
||||
has(id: string): boolean {
|
||||
if (this.loadedKeyMetadata.has(id)) {
|
||||
return this.loadedKeyMetadata.get(id);
|
||||
}
|
||||
|
||||
return this.data.has(id);
|
||||
}
|
||||
|
||||
async loadAll(): Promise<Map<string, RoleInfo>> {
|
||||
await this.load(RolesResource.keyAll);
|
||||
await this.load(CachedMapAllKey);
|
||||
return this.data;
|
||||
}
|
||||
|
||||
async refreshAll(): Promise<Map<string, RoleInfo>> {
|
||||
await this.refresh(RolesResource.keyAll);
|
||||
await this.refresh(CachedMapAllKey);
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@@ -96,11 +84,13 @@ export class RolesResource extends CachedMapResource<string, RoleInfo> {
|
||||
}
|
||||
|
||||
protected async loader(key: ResourceKey<string>): Promise<Map<string, RoleInfo>> {
|
||||
const all = ResourceKeyUtils.hasMark(key, RolesResource.keyAll.mark);
|
||||
const all = ResourceKeyUtils.includes(key, CachedMapAllKey);
|
||||
|
||||
await ResourceKeyUtils.forEachAsync(all ? CachedMapAllKey : key, async key => {
|
||||
const roleId = all ? undefined : key;
|
||||
|
||||
await ResourceKeyUtils.forEachAsync(all ? RolesResource.keyAll : key, async key => {
|
||||
const { roles } = await this.graphQLService.sdk.getRolesList({
|
||||
roleId: !all ? key : undefined,
|
||||
roleId,
|
||||
});
|
||||
|
||||
if (all) {
|
||||
@@ -108,10 +98,6 @@ export class RolesResource extends CachedMapResource<string, RoleInfo> {
|
||||
}
|
||||
|
||||
this.updateRoles(...roles);
|
||||
|
||||
if (all) {
|
||||
this.loadedKeyMetadata.set(RolesResource.keyAll.list[0], true);
|
||||
}
|
||||
});
|
||||
|
||||
return this.data;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 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 ILoadableState {
|
||||
isLoading: () => boolean;
|
||||
isLoaded: () => boolean;
|
||||
exception?: Error[] | Error | null;
|
||||
reload?: () => void;
|
||||
}
|
||||
@@ -6,16 +6,19 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
import { Translate } from '@cloudbeaver/core-localization';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import { ExceptionMessage } from '../ExceptionMessage';
|
||||
import { StaticImage } from '../StaticImage';
|
||||
import { ILoaderContext, LoaderContext } from './LoaderContext';
|
||||
import { loaderStyles, overlayStyles } from './loaderStyles';
|
||||
|
||||
export interface ILoadableState {
|
||||
@@ -77,6 +80,10 @@ export const Loader = observer<Props>(function Loader({
|
||||
children,
|
||||
onCancel,
|
||||
}) {
|
||||
const context = useContext(LoaderContext);
|
||||
const [loaderId] = useState(() => uuid());
|
||||
const [contextState] = useState<ILoaderContext>(() => ({ state: observable(new Set<string>()) }));
|
||||
|
||||
let exception: Error | null = null;
|
||||
let reload: (() => void) | undefined;
|
||||
|
||||
@@ -116,6 +123,7 @@ export const Loader = observer<Props>(function Loader({
|
||||
style = useStyles(loaderStyles, style, overlay && overlayStyles);
|
||||
const [isVisible, setVisible] = useState(loading);
|
||||
const spinnerURL = (secondary || overlay) ? spinnerType.secondary : spinnerType.primary;
|
||||
const refLoaderDisplayed = { state: false };
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
@@ -126,21 +134,40 @@ export const Loader = observer<Props>(function Loader({
|
||||
const id = setTimeout(() => {
|
||||
setVisible(loading);
|
||||
}, 500);
|
||||
return () => clearTimeout(id);
|
||||
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}, [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (context) {
|
||||
if (refLoaderDisplayed.state) {
|
||||
context.state.add(loaderId);
|
||||
} else {
|
||||
context.state.delete(loaderId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => () => {
|
||||
if (context) {
|
||||
context.state.delete(loaderId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (exception && !loading) {
|
||||
return styled(style)(
|
||||
<ExceptionMessage exception={exception} inline={inline || inlineException} onRetry={reload} />
|
||||
);
|
||||
}
|
||||
|
||||
if (children && (!loader || !loading)) {
|
||||
if (children && (!loader || !loading) && !overlay) {
|
||||
if (loaded) {
|
||||
if (typeof children === 'function') {
|
||||
return <>{children()}</>;
|
||||
return <LoaderContext.Provider value={contextState}>{children()}</LoaderContext.Provider>;
|
||||
} else {
|
||||
return <>{children}</>;
|
||||
return <LoaderContext.Provider value={contextState}>{children}</LoaderContext.Provider>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,25 +176,35 @@ export const Loader = observer<Props>(function Loader({
|
||||
}
|
||||
}
|
||||
|
||||
if ((!isVisible && overlay) || !loading) {
|
||||
if ((!isVisible && overlay) || !loading || contextState.state.size > 0) {
|
||||
if (overlay) {
|
||||
return <LoaderContext.Provider value={contextState}>{children}</LoaderContext.Provider>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
refLoaderDisplayed.state = true;
|
||||
|
||||
return styled(style)(
|
||||
<loader className={className} {...use({ small, fullSize, inline })}>
|
||||
<icon><StaticImage icon={spinnerURL} /></icon>
|
||||
{!hideMessage && <message><Translate token={message || 'ui_processing_loading'} /></message>}
|
||||
{onCancel && (
|
||||
<actions>
|
||||
<Button
|
||||
<LoaderContext.Provider value={contextState}>
|
||||
{overlay && children}
|
||||
<loader className={className} {...use({ small, fullSize, inline })}>
|
||||
<icon><StaticImage icon={spinnerURL} /></icon>
|
||||
{!hideMessage && <message><Translate token={message || 'ui_processing_loading'} /></message>}
|
||||
{onCancel && (
|
||||
<actions>
|
||||
<Button
|
||||
type="button"
|
||||
mod={['unelevated']}
|
||||
disabled={cancelDisabled}
|
||||
onClick={onCancel}
|
||||
><Translate token='ui_processing_cancel' />
|
||||
</Button>
|
||||
</actions>
|
||||
)}
|
||||
</loader>
|
||||
>
|
||||
<Translate token='ui_processing_cancel' />
|
||||
</Button>
|
||||
</actions>
|
||||
)}
|
||||
</loader>
|
||||
</LoaderContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 { createContext } from 'react';
|
||||
|
||||
export interface ILoaderContext {
|
||||
state: Set<string>;
|
||||
}
|
||||
|
||||
export const LoaderContext = createContext<ILoaderContext | null>(null);
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 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 { ILoadableState } from './ILoadableState';
|
||||
|
||||
export type LoaderState = ILoadableState | {
|
||||
loading: boolean;
|
||||
};
|
||||
+2
-2
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration';
|
||||
import { BASE_CONTAINERS_STYLES, ToolsAction, Loader, ToolsPanel } from '@cloudbeaver/core-blocks';
|
||||
import { BASE_CONTAINERS_STYLES, ToolsAction, ToolsPanel } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
@@ -104,10 +104,10 @@ export const RolesPage = observer<Props>(function RolesPage({
|
||||
<layout-grid-cell {...use({ span: 12 })}>
|
||||
<RolesTable
|
||||
roles={table.roles}
|
||||
state={table.state}
|
||||
selectedItems={table.tableState.selected}
|
||||
expandedItems={table.tableState.expanded}
|
||||
/>
|
||||
<Loader loading={table.processing} overlay />
|
||||
</layout-grid-cell>
|
||||
</layout-grid-inner>
|
||||
</layout-grid>
|
||||
|
||||
+29
-19
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import type { RoleInfo } from '@cloudbeaver/core-authentication';
|
||||
import { Table, TableHeader, TableColumnHeader, TableBody, TableSelect } from '@cloudbeaver/core-blocks';
|
||||
import { Table, TableHeader, TableColumnHeader, TableBody, TableSelect, Loader, ILoadableState } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
@@ -19,36 +19,46 @@ import { Role } from './Role';
|
||||
const styles = css`
|
||||
Table {
|
||||
width: 100%;
|
||||
min-height: 140px; /* loader overlay size */
|
||||
}
|
||||
`;
|
||||
|
||||
const loaderStyle = css`
|
||||
ExceptionMessage {
|
||||
padding: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
roles: RoleInfo[];
|
||||
state: ILoadableState;
|
||||
selectedItems: Map<string, boolean>;
|
||||
expandedItems: Map<string, boolean>;
|
||||
}
|
||||
|
||||
export const RolesTable = observer<Props>(function RolesTable({ roles, selectedItems, expandedItems }) {
|
||||
export const RolesTable = observer<Props>(function RolesTable({ roles, state, selectedItems, expandedItems }) {
|
||||
const translate = useTranslate();
|
||||
const keys = roles.map(role => role.roleId);
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<Table keys={keys} selectedItems={selectedItems} expandedItems={expandedItems} {...use({ size: 'big' })}>
|
||||
<TableHeader>
|
||||
<TableColumnHeader min flex centerContent>
|
||||
<TableSelect />
|
||||
</TableColumnHeader>
|
||||
<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>
|
||||
<Loader state={state} style={loaderStyle} overlay>
|
||||
<Table keys={keys} selectedItems={selectedItems} expandedItems={expandedItems} {...use({ size: 'big' })}>
|
||||
<TableHeader>
|
||||
<TableColumnHeader min flex centerContent>
|
||||
<TableSelect />
|
||||
</TableColumnHeader>
|
||||
<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>
|
||||
</Loader>
|
||||
);
|
||||
});
|
||||
|
||||
+8
-6
@@ -9,17 +9,18 @@
|
||||
import { computed, observable } from 'mobx';
|
||||
|
||||
import { compareRoles, RoleInfo, RolesResource } from '@cloudbeaver/core-authentication';
|
||||
import { TableState, useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { ILoadableState, TableState, useMapResource, useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
|
||||
interface State {
|
||||
tableState: TableState;
|
||||
processing: boolean;
|
||||
roles: RoleInfo[];
|
||||
state: ILoadableState;
|
||||
update: () => Promise<void>;
|
||||
delete: () => Promise<void>;
|
||||
}
|
||||
@@ -27,15 +28,16 @@ interface State {
|
||||
export function useRolesTable(): Readonly<State> {
|
||||
const notificationService = useService(NotificationService);
|
||||
const dialogService = useService(CommonDialogService);
|
||||
const resource = useService(RolesResource);
|
||||
const resource = useMapResource(useRolesTable, RolesResource, CachedMapAllKey);
|
||||
|
||||
const translate = useTranslate();
|
||||
|
||||
return useObservableRef<State>(() => ({
|
||||
tableState: new TableState(),
|
||||
processing: false,
|
||||
state: resource,
|
||||
get roles() {
|
||||
return resource.values.slice().sort(compareRoles);
|
||||
return resource.resource.values.slice().sort(compareRoles);
|
||||
},
|
||||
async update() {
|
||||
if (this.processing) {
|
||||
@@ -44,7 +46,7 @@ export function useRolesTable(): Readonly<State> {
|
||||
|
||||
try {
|
||||
this.processing = true;
|
||||
await resource.refreshAll();
|
||||
await resource.resource.refreshAll();
|
||||
notificationService.logSuccess({ title: 'administration_roles_role_list_update_success' });
|
||||
} catch (exception) {
|
||||
notificationService.logException(exception, 'administration_roles_role_list_update_fail');
|
||||
@@ -77,7 +79,7 @@ export function useRolesTable(): Readonly<State> {
|
||||
|
||||
try {
|
||||
this.processing = true;
|
||||
await resource.deleteRole(resourceKeyList(deletionList));
|
||||
await resource.resource.deleteRole(resourceKeyList(deletionList));
|
||||
|
||||
this.tableState.unselect();
|
||||
this.tableState.unexpand(deletionList);
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ export const UsersAdministration: AdministrationItemContentComponent = observer(
|
||||
}
|
||||
|
||||
return styled(style)(
|
||||
<TabsState selectedId={subName} onChange={openSub}>
|
||||
<TabsState selectedId={subName} lazy onChange={openSub}>
|
||||
<ToolsPanel>
|
||||
<TabList style={style}>
|
||||
<Tab tabId={EUsersAdministrationSub.Users} style={style}>{translate('authentication_administration_item_users')}</Tab>
|
||||
|
||||
+1
-18
@@ -7,10 +7,9 @@
|
||||
*/
|
||||
|
||||
import { AdministrationItemService } from '@cloudbeaver/core-administration';
|
||||
import { AdminUser, RolesResource, UsersResource } from '@cloudbeaver/core-authentication';
|
||||
import { AdminUser, 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 './Roles/CreateRoleService';
|
||||
import { UsersAdministration } from './UsersAdministration';
|
||||
@@ -29,8 +28,6 @@ export class UsersAdministrationService extends Bootstrap {
|
||||
|
||||
constructor(
|
||||
private administrationItemService: AdministrationItemService,
|
||||
private notificationService: NotificationService,
|
||||
private usersResource: UsersResource,
|
||||
private createUserService: CreateUserService,
|
||||
private readonly rolesResource: RolesResource,
|
||||
private readonly createRoleService: CreateRoleService,
|
||||
@@ -62,7 +59,6 @@ export class UsersAdministrationService extends Bootstrap {
|
||||
],
|
||||
getContentComponent: () => UsersAdministration,
|
||||
getDrawerComponent: () => UsersDrawerItem,
|
||||
onActivate: this.loadUsers.bind(this),
|
||||
});
|
||||
this.userDetailsInfoPlaceholder.add(Origin, 0);
|
||||
}
|
||||
@@ -75,22 +71,9 @@ export class UsersAdministrationService extends Bootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
private async loadUsers() {
|
||||
try {
|
||||
await this.usersResource.loadAll();
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, 'Error occurred while loading users');
|
||||
}
|
||||
}
|
||||
|
||||
private async loadRoles(param: string | null) {
|
||||
if (param === 'create') {
|
||||
this.createRoleService.fillData();
|
||||
}
|
||||
try {
|
||||
await this.rolesResource.loadAll();
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, 'Error occurred while loading roles');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-24
@@ -11,9 +11,7 @@ import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration';
|
||||
import { AdminUser, UsersResource } from '@cloudbeaver/core-authentication';
|
||||
import {
|
||||
Table, TableHeader, TableColumnHeader, TableBody, TableSelect, getComputed, useMapResource, ToolsAction, ToolsPanel
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { Table, TableHeader, TableColumnHeader, TableBody, TableSelect, getComputed, useMapResource, ToolsAction, ToolsPanel, Loader } from '@cloudbeaver/core-blocks';
|
||||
import { useController, useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
|
||||
@@ -52,15 +50,18 @@ const layoutStyles = composes(
|
||||
`
|
||||
);
|
||||
|
||||
const loaderStyle = css`
|
||||
ExceptionMessage {
|
||||
padding: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const styles = css`
|
||||
Table {
|
||||
width: 100%;
|
||||
min-height: 140px; /* loader overlay size */
|
||||
}
|
||||
|
||||
Loader {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ToolsPanel {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -137,27 +138,29 @@ export const UsersTable = observer<Props>(function UsersTable({ sub, param }) {
|
||||
</layout-grid-cell>
|
||||
)}
|
||||
<layout-grid-cell {...use({ span: 12 })}>
|
||||
<Table
|
||||
<Loader style={loaderStyle} state={usersResource} overlay>
|
||||
<Table
|
||||
keys={keys}
|
||||
selectedItems={controller.selectedItems}
|
||||
expandedItems={controller.expandedItems}
|
||||
{...use({ size: 'big' })}
|
||||
>
|
||||
<TableHeader>
|
||||
{isLocalProviderAvailable && (
|
||||
<TableColumnHeader min flex centerContent>
|
||||
<TableSelect />
|
||||
</TableColumnHeader>
|
||||
)}
|
||||
<TableColumnHeader min />
|
||||
<TableColumnHeader>{translate('authentication_user_name')}</TableColumnHeader>
|
||||
<TableColumnHeader>{translate('authentication_user_role')}</TableColumnHeader>
|
||||
<TableColumnHeader />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map(user => <User key={user.userId} user={user} selectable={isLocalProviderAvailable} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
>
|
||||
<TableHeader>
|
||||
{isLocalProviderAvailable && (
|
||||
<TableColumnHeader min flex centerContent>
|
||||
<TableSelect />
|
||||
</TableColumnHeader>
|
||||
)}
|
||||
<TableColumnHeader min />
|
||||
<TableColumnHeader>{translate('authentication_user_name')}</TableColumnHeader>
|
||||
<TableColumnHeader>{translate('authentication_user_role')}</TableColumnHeader>
|
||||
<TableColumnHeader />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map(user => <User key={user.userId} user={user} selectable={isLocalProviderAvailable} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Loader>
|
||||
</layout-grid-cell>
|
||||
</layout-grid-inner>
|
||||
</layout-grid>
|
||||
|
||||
Reference in New Issue
Block a user