refactor: CB-1452 loader context

This commit is contained in:
Wroud
2021-11-19 16:57:29 +03:00
parent 256df04921
commit 661ebc94f5
11 changed files with 173 additions and 110 deletions
@@ -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;
};
@@ -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>
@@ -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>
);
});
@@ -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);
@@ -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>
@@ -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');
}
}
}
@@ -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>