feat: CB-1216 CB-1217 CB-1239 metadata editor update

This commit is contained in:
Wroud
2021-09-10 19:04:50 +03:00
parent 1521b9109f
commit f73a2f8b02
25 changed files with 357 additions and 267 deletions
@@ -31,16 +31,11 @@ interface INavigationNode {
filterValue: string;
}
export function useNavigationNode({ id }: NavNode): INavigationNode {
export function useNavigationNode(node: NavNode): INavigationNode {
const contextRef = useObjectRef({
context: useContext(TreeContext),
});
const { node, isLoading } = useNode(id);
// TODO: hack to provide actual node information
if (!node) {
throw new Error('Node should exists');
}
const { isLoading } = useNode(node.id);
const children = useChildren(node.id);
const loading = isLoading() || children.isLoading();
@@ -58,7 +58,7 @@ export const ConnectionSelector = observer(function ConnectionSelector() {
const style = useStyles(styles);
const controller = useController(ConnectionSelectorController);
const isEnabled = usePermission(EPermission.public);
const driver = useMapResource(DBDriverResource, null, { onLoad: resource => isEnabled && resource.loadAll() });
const driver = useMapResource(DBDriverResource, null, { onLoad: resource => { isEnabled && resource.loadAll(); } });
const ConnectionMenu = TopMenuItem;
const SchemaOrCatalogMenu = TopMenuItem;
+1 -1
View File
@@ -14,7 +14,7 @@ export * from './shared/NodesManager/extensions/IObjectSchemaSetter';
export * from './shared/NodesManager/NavNodeView/IFolderTransform';
export * from './shared/NodesManager/NavNodeView/NavNodeViewService';
export * from './shared/NodesManager/ConnectionDialogsService';
export * from './shared/NodesManager/DBObjectService';
export * from './shared/NodesManager/DBObjectResource';
export * from './shared/NodesManager/NavNodeContextMenuService';
export * from './shared/NodesManager/NavNodeInfoResource';
export * from './shared/NodesManager/NavNodeManagerService';
@@ -15,7 +15,7 @@ import type { DBObject } from './EntityTypes';
import { NavNodeInfoResource } from './NavNodeInfoResource';
@injectable()
export class DBObjectService extends CachedMapResource<string, DBObject> {
export class DBObjectResource extends CachedMapResource<string, DBObject> {
constructor(
private graphQLService: GraphQLService,
private navNodeInfoResource: NavNodeInfoResource
@@ -195,7 +195,7 @@ export class NavNodeContextMenuService extends Bootstrap {
onClick: async context => {
const node = context.data.node;
try {
await this.navNodeManagerService.refreshTree(node.id);
await this.navNodeManagerService.refreshTree(node.parentId);
} catch (exception) {
this.notificationService.logException(exception, 'Failed to refresh node');
}
@@ -227,14 +227,10 @@ export class NavNodeContextMenuService extends Bootstrap {
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
if (name !== result) {
const notification = this.notificationService.processNotification(() => ProcessSnackbar, {}, { title: 'ui_rename_processing' });
try {
await this.navTreeResource.changeName(node, result);
const message = `prev: ${name}\nnew: ${result}`;
notification.controller.resolve(`${node.nodeType} was renamed`, message);
} catch (exception) {
notification.controller.reject(exception, 'Error occured while renaming');
this.notificationService.logException(exception, 'Error occurred while renaming');
}
}
}
@@ -130,7 +130,7 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
}
async changeName(node: NavNode, name: string): Promise<void> {
await this.performUpdate(node.id, [], async () => {
await this.performUpdate(resourceKeyList([node.id, node.parentId]), [], async () => {
await this.graphQLService.sdk.navRenameNode({
nodePath: node.id,
newName: name,
@@ -213,10 +213,11 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
for (const id of items) {
this.data.delete(id);
}
const allKeys = resourceKeyList(items);
this.markUpdated(allKeys);
this.onItemDelete.execute(allKeys);
this.navNodeInfoResource.delete(ResourceKeyUtils.exclude(allKeys, key));
this.navNodeInfoResource.delete(allKeys);
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, string[]>> {
@@ -263,7 +264,7 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
const connectionInfo = this.connectionInfo.get(key);
if (!connectionInfo?.connected) {
this.delete(nodeId);
this.delete(resourceKeyList(this.get(nodeId) || []));
} else {
await this.markTreeOutdated(nodeId);
}
@@ -8,13 +8,13 @@
import { useService } from '@cloudbeaver/core-di';
import { DBObjectService } from './DBObjectService';
import { DBObjectResource } from './DBObjectResource';
export function useDatabaseObjectInfo(navNodeId: string) {
const dbObjectService = useService(DBObjectService);
const dbObject = dbObjectService.get(navNodeId);
const isLoading = dbObjectService.isDataLoading(navNodeId);
const isLoaded = dbObjectService.isLoaded(navNodeId);
const dbObjectResource = useService(DBObjectResource);
const dbObject = dbObjectResource.get(navNodeId);
const isLoading = dbObjectResource.isDataLoading(navNodeId);
const isLoaded = dbObjectResource.isLoaded(navNodeId);
return { dbObject, isLoading, isLoaded };
}
@@ -94,17 +94,18 @@ interface Props {
message?: string;
exception?: Error;
inline?: boolean;
className?: string;
onRetry?: () => void;
}
export const ExceptionMessage = observer<Props>(function ExceptionMessage({
name, message, exception = null, inline, onRetry,
name, message, exception = null, inline, className, onRetry,
}) {
const translate = useTranslate();
const error = useErrorDetails(exception);
return styled(styles)(
<error {...use({ inline })}>
<error {...use({ inline })} className={className}>
<error-icon><IconOrImage icon={inline ? '/icons/error_icon_sm.svg' : '/icons/error_icon.svg'} /></error-icon>
<error-data>
<error-name><span>{name || error.details?.name}</span></error-name>
@@ -11,7 +11,7 @@ import { useState, useEffect } from 'react';
import styled, { use } from 'reshadow';
import { Translate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { Button } from '../Button';
import { ExceptionMessage } from '../ExceptionMessage';
@@ -49,6 +49,7 @@ interface Props {
className?: string;
fullSize?: boolean;
state?: LoaderState | LoaderState[];
style?: ComponentStyle;
children?: () => React.ReactNode;
onCancel?: () => void;
}
@@ -72,6 +73,7 @@ export const Loader = observer<Props>(function Loader({
loading = true,
inlineException,
state,
style,
children,
onCancel,
}) {
@@ -111,7 +113,7 @@ export const Loader = observer<Props>(function Loader({
}
}
const style = useStyles(loaderStyles, overlay && overlayStyles);
style = useStyles(loaderStyles, style, overlay && overlayStyles);
const [isVisible, setVisible] = useState(loading);
const spinnerURL = (secondary || overlay) ? spinnerType.secondary : spinnerType.primary;
@@ -128,7 +130,9 @@ export const Loader = observer<Props>(function Loader({
}, [loading]);
if (exception && !loading) {
return <ExceptionMessage exception={exception} inline={inline || inlineException} onRetry={reload} />;
return styled(style)(
<ExceptionMessage exception={exception} inline={inline || inlineException} onRetry={reload} />
);
}
if (children && (!loader || !loading)) {
@@ -6,6 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { useEffect, useState } from 'react';
import { IServiceConstructor, useService } from '@cloudbeaver/core-di';
@@ -13,15 +14,15 @@ import { NotificationService } from '@cloudbeaver/core-events';
import { CachedResourceIncludeArgs, CachedMapResource, CachedMapResourceGetter, ResourceKey, CachedMapResourceValue, CachedMapResourceKey, CachedMapResourceArguments, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter } from '@cloudbeaver/core-sdk';
import type { ILoadableState } from './Loader/Loader';
import { useObjectRef } from './useObjectRef';
import { useObservableRef } from './useObservableRef';
interface IActions<
TKeyArg extends ResourceKey<CachedMapResourceKey<TResource>>,
TResource extends CachedMapResource<any, any, any>,
TIncludes
> {
isActive?: () => Promise<boolean> | boolean;
onLoad?: (resource: TResource) => Promise<any> | any;
isActive?: (resource: TResource) => Promise<boolean> | boolean;
onLoad?: (resource: TResource) => Promise<boolean | void> | boolean | void;
onData?: (
data: CachedMapResourceLoader<
TKeyArg,
@@ -129,7 +130,7 @@ export function useMapResource<
includes = keyObj.includes;
}
const refObj = useObjectRef(() => ({
const refObj = useObservableRef(() => ({
loading: false,
prevData: undefined as CachedMapResourceLoader<
TKeyArg,
@@ -137,8 +138,52 @@ export function useMapResource<
CachedMapResourceValue<TResource>,
TIncludes
> | undefined,
load: () => {},
async load() {
const { resource, actions, prevData, key, includes } = this;
const active = await actions?.isActive?.(resource);
if (this.loading || active === false) {
return;
}
this.loading = true;
try {
const prevent = await actions?.onLoad?.(resource);
if (key === null || prevent === true) {
setException(null);
return;
}
const newData = await resource.load(key, includes as any);
setException(null);
try {
await actions?.onData?.(
newData,
resource,
prevData
);
} finally {
this.prevData = newData;
}
} catch (exception) {
if (resource.getException(key) === null) {
setException(exception);
}
actions?.onError?.(exception);
if (!this.exceptionObserved) {
notifications.logException(exception, 'Can\'t load data');
}
} finally {
this.loading = false;
}
},
}), {
loading: observable.ref,
}, {
exceptionObserved: false,
resource,
key,
@@ -149,49 +194,6 @@ export function useMapResource<
const outdated = resource.isOutdated(key);
refObj.load = async function load() {
const { loading, resource, actions, prevData } = refObj;
const active = await actions?.isActive?.();
if (loading || active === false) {
return;
}
this.loading = true;
try {
await actions?.onLoad?.(resource);
if (key === null) {
return;
}
const newData = await resource.load(key, includes as any);
setException(null);
try {
await actions?.onData?.(
newData,
resource,
prevData
);
} finally {
refObj.prevData = newData;
}
} catch (exception) {
if (resource.getException(key) === null) {
setException(exception);
}
actions?.onError?.(exception);
if (!refObj.exceptionObserved) {
notifications.logException(exception, 'Can\'t load data');
}
} finally {
this.loading = false;
}
};
const [result] = useState<
IMapResourceResult<TResource, TIncludes>
| IMapResourceListResult<TResource, TIncludes>
@@ -226,7 +228,7 @@ export function useMapResource<
return false;
}
return resource.isDataLoading(refObj.key);
return refObj.loading || resource.isDataLoading(refObj.key);
},
}));
@@ -7,7 +7,7 @@
*/
import { injectable } from '@cloudbeaver/core-di';
import { SessionDataResource } from '@cloudbeaver/core-root';
import { EPermission, PermissionsResource, SessionDataResource } from '@cloudbeaver/core-root';
import {
GraphQLService,
CachedMapResource,
@@ -30,6 +30,7 @@ export class ConnectionExecutionContextResource extends CachedMapResource<string
constructor(
private graphQLService: GraphQLService,
private connectionInfoResource: ConnectionInfoResource,
private permissionsResource: PermissionsResource,
sessionDataResource: SessionDataResource
) {
super();
@@ -39,6 +40,7 @@ export class ConnectionExecutionContextResource extends CachedMapResource<string
sessionDataResource.onDataUpdate.addHandler(async () => {
await this.load(ConnectionExecutionContextResource.keyAll);
});
this.permissionsResource.onDataOutdated.addHandler(this.markOutdated.bind(this));
connectionInfoResource.onItemAdd.addHandler(this.updateConnectionContexts.bind(this));
connectionInfoResource.onItemDelete.addHandler(this.deleteConnectionContexts.bind(this));
@@ -168,6 +170,10 @@ export class ConnectionExecutionContextResource extends CachedMapResource<string
protected async loader(
key: ResourceKey<string>
): Promise<Map<string, IConnectionExecutionContextInfo>> {
if (!this.permissionsResource.has(EPermission.public)) {
return this.data;
}
const all = ResourceKeyUtils.hasMark(key, ConnectionExecutionContextResource.keyAll.mark);
await ResourceKeyUtils.forEachAsync(all ? ConnectionExecutionContextResource.keyAll : key, async contextId => {
@@ -75,11 +75,11 @@ export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps>
|| state.initialGrantedSubjects.some(subject => !state.grantedSubjects.includes(subject));
const users = useMapResource(UsersResource, null, {
onLoad: resource => resource.loadAll(),
onLoad: resource => { resource.loadAll(); },
});
const roles = useMapResource(RolesResource, null, {
onLoad: resource => resource.loadAll(),
onLoad: resource => { resource.loadAll(); },
});
const grantedUsers = useMemo(() => computed(() => users.resource.values
@@ -107,6 +107,9 @@ export abstract class CachedMapResource<
getException(key: ResourceKeyList<TKey>): Array<Error | null>;
getException(key: ResourceKey<TKey>): Array<Error | null>| Error | null;
getException(key: ResourceKey<TKey>): Array<Error | null>| Error | null {
if (ResourceKeyUtils.some(key, key => !this.has(key))) { // TODO: metadata isn't properly cleared while rename with opened in Metadata Editor node
return null;
}
key = this.transformParam(key);
return ResourceKeyUtils.map(key, key => this.metadata.get(key).exception);
}
@@ -47,6 +47,7 @@ export interface ResourceKeyUtils {
map: MapFnc;
includes: <TKey>(first: ResourceKey<TKey>, second: ResourceKey<TKey>) => boolean;
exclude: <TKey>(first: ResourceKeyList<TKey>, second: ResourceKey<TKey>) => ResourceKey<TKey>;
join: <TKey>(first: ResourceKey<TKey>, second: ResourceKey<TKey>) => ResourceKey<TKey>;
}
// eslint-disable-next-line @typescript-eslint/no-redeclare
@@ -151,6 +152,24 @@ export const ResourceKeyUtils: ResourceKeyUtils = {
return resourceKeyList(param.list.filter(param => param !== key), param.mark);
},
join<TKey>(param: ResourceKey<TKey>, key: ResourceKey<TKey>): ResourceKey<TKey> {
const list: TKey[] = [];
if (isResourceKeyList(param)) {
list.push(...param.list);
} else {
list.push(param);
}
if (isResourceKeyList(key)) {
list.push(...key.list);
} else {
list.push(key);
}
return resourceKeyList(list);
},
};
export function isResourceKeyList<T>(data: any): data is ResourceKeyList<T> {
@@ -77,7 +77,7 @@ export class DataViewerTabService {
return;
}
const node = await this.navNodeManagerService.loadNode({
const node = this.navNodeManagerService.getNode({
nodeId: tab.handlerState.objectId,
parentId: tab.handlerState.parentId,
});
@@ -7,10 +7,11 @@
*/
import { observer } from 'mobx-react-lite';
import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import type { NavNodeTransformViewComponent } from '@cloudbeaver/core-app';
import { Loader, useTab } from '@cloudbeaver/core-blocks';
import { Loader } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { SQLCodeEditorLoader } from '@cloudbeaver/plugin-sql-editor';
@@ -32,7 +33,12 @@ const styles = css`
export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(function DDLViewerTabPanel({ nodeId, folderId }) {
const controller = useController(DdlViewerController, nodeId);
useTab(folderId, () => controller.load());
useEffect(() => {
controller.load();
});
// TODO: not triggered in switch case with lazy
// useTab(folderId, () => controller.load());
if (controller.isLoading) {
return <Loader />;
@@ -9,9 +9,10 @@
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { NavNodeViewService, useNode } from '@cloudbeaver/core-app';
import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-blocks';
import { DBObjectResource, NavNodeViewService, useChildren, useNode } from '@cloudbeaver/core-app';
import { getComputed, Loader, Tab, TabIcon, TabTitle, useStateDelay } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
interface IFolderTabRendererProps {
@@ -45,11 +46,23 @@ interface INavNodeTabProps {
const NavNodeTab = observer<INavNodeTabProps>(function NavNodeTab({ nodeId, style }) {
const nodeInfo = useNode(nodeId);
const children = useChildren(nodeId);
const dbObjectResource = useService(DBObjectResource);
const childrenList = resourceKeyList(children.children || []);
const loading = useStateDelay(getComputed(() => (
nodeInfo.isLoaded() && nodeInfo.isLoading()
) || (
children.isLoaded() && children.isLoading()
) || (
dbObjectResource.isLoaded(childrenList) && dbObjectResource.isDataLoading(childrenList)
)), 300);
return styled(useStyles(style))(
<Tab tabId={nodeId}>
{nodeInfo.node?.icon && <TabIcon icon={nodeInfo.node.icon} />}
<TabTitle>{nodeInfo.node?.name}</TabTitle>
<tab-loader><Loader loading={loading} small /></tab-loader>
</Tab>
);
});
@@ -9,8 +9,8 @@
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { useDatabaseObjectInfo } from '@cloudbeaver/core-app';
import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoFormNew, Group } from '@cloudbeaver/core-blocks';
import { DBObjectResource } from '@cloudbeaver/core-app';
import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoFormNew, Group, useMapResource } from '@cloudbeaver/core-blocks';
import { BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
@@ -26,42 +26,43 @@ export const ObjectProperties = observer<Props>(function ObjectProperties({
objectId,
}) {
const translate = useTranslate();
const { dbObject, isLoading } = useDatabaseObjectInfo(objectId);
const dbObject = useMapResource(DBObjectResource, objectId);
const styles = useStyles(BASE_CONTAINERS_STYLES);
const { categories, isUncategorizedExists } = useObjectPropertyCategories(dbObject?.properties ?? emptyArray);
const properties = dbObject?.properties;
if (!properties && isLoading) {
return <Loader />;
}
if (!properties || properties.length === 0) {
return <TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>;
}
const { categories, isUncategorizedExists } = useObjectPropertyCategories(dbObject.data?.properties ?? emptyArray);
const properties = dbObject.data?.properties;
return styled(styles)(
<ColoredContainer overflow parent gap>
{isUncategorizedExists && (
<Group gap large>
<ObjectPropertyInfoFormNew
properties={properties}
category={null}
small
readOnly
/>
</Group>
)}
{categories.map(category => (
<Group key={category} gap large>
<GroupTitle>{category}</GroupTitle>
<ObjectPropertyInfoFormNew
properties={properties}
category={category}
small
readOnly
/>
</Group>
))}
</ColoredContainer>
<Loader state={dbObject}>{() => styled(styles)(
<>
{!properties || properties.length === 0 ? (
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
) : (
<ColoredContainer overflow parent gap>
{isUncategorizedExists && (
<Group gap large>
<ObjectPropertyInfoFormNew
properties={properties}
category={null}
small
readOnly
/>
</Group>
)}
{categories.map(category => (
<Group key={category} gap large>
<GroupTitle>{category}</GroupTitle>
<ObjectPropertyInfoFormNew
properties={properties}
category={category}
small
readOnly
/>
</Group>
))}
</ColoredContainer>
)}
</>
)}
</Loader>
);
});
@@ -8,7 +8,8 @@
import styled, { css } from 'reshadow';
import { NavNodeInfoResource, NavNodeTransformViewComponent, useChildren } from '@cloudbeaver/core-app';
import { DBObjectResource, NavNodeInfoResource, NavNodeTransformViewComponent, NavTreeResource } from '@cloudbeaver/core-app';
import { Loader, useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
@@ -29,16 +30,27 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = function Virtua
}) {
const nodeType = VirtualFolderUtils.getNodeType(folderId);
const navNodeInfoResource = useService(NavNodeInfoResource);
const children = useChildren(nodeId);
const tree = useMapResource(NavTreeResource, nodeId);
const key = resourceKeyList([nodeId, ...tree.data || []]);
const dbObject = useMapResource(DBObjectResource, key, {
async onLoad(resource: DBObjectResource) {
await resource.loadChildren(nodeId, key);
},
});
const nodeIds = navNodeInfoResource
.get(resourceKeyList(children.children || []))
.get(resourceKeyList(tree.data || []))
.filter(node => node?.nodeType === nodeType)
.map(node => node!.id);
return styled(style)(
<tab-wrapper>
<ObjectChildrenPropertyTable nodeIds={nodeIds} />
</tab-wrapper>
<Loader state={[tree, dbObject]}>{() => styled(style)(
<>
<tab-wrapper>
<ObjectChildrenPropertyTable nodeIds={nodeIds} />
</tab-wrapper>
</>
)}
</Loader>
);
};
@@ -11,8 +11,9 @@ import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import { ITab, NavNodeManagerService, NavNodeViewService, NavTreeResource } from '@cloudbeaver/core-app';
import { ITabData, TabList, TabPanel, TabsState, useMapResource, verticalTabStyles } from '@cloudbeaver/core-blocks';
import { ITabData, Loader, TabList, TabPanel, TabsState, TextPlaceholder, useMapResource, verticalTabStyles } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import type { IObjectViewerTabState } from '../IObjectViewerTabState';
@@ -45,6 +46,19 @@ const styles = composes(
TabList {
border-right: 1px solid;
}
TabTitle {
flex: 1;
}
tab-loader {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 16px;
overflow: hidden;
}
ExceptionMessage {
padding: 24px;
}
`
);
@@ -55,20 +69,16 @@ interface IProps {
}
export const ObjectFolders = observer<IProps>(function ObjectFolders({ tab }) {
const translate = useTranslate();
const navNodeManagerService = useService(NavNodeManagerService);
const navNodeViewService = useService(NavNodeViewService);
const style = useStyles(verticalTabStyles, styles);
const nodeId = tab.handlerState.objectId;
const parentId = tab.handlerState.parentId;
let folderId = tab.handlerState.folderId;
useMapResource(NavTreeResource, nodeId, {
onLoad: async resource => {
for (const nodeId of tab.handlerState.parents) {
await resource.load(nodeId);
}
},
});
const children = useMapResource(NavTreeResource, nodeId);
const folders = navNodeViewService.getFolders(nodeId) || [];
@@ -86,20 +96,29 @@ export const ObjectFolders = observer<IProps>(function ObjectFolders({ tab }) {
folderId = folders[0];
}
return styled(useStyles(verticalTabStyles, styles))(
<TabsState currentTabId={folderId} orientation='vertical' onChange={openFolder}>
<vertical-tabs>
<TabList aria-label="Object folders">
{folders.map(folderId => (
<FolderTabRenderer key={folderId} nodeId={nodeId} folderId={folderId} style={tabStyles} />
))}
</TabList>
{folders.map(folderId => (
<TabPanel key={folderId} tabId={folderId}>
<FolderPanelRenderer key={folderId} nodeId={nodeId} folderId={folderId} style={tabStyles} />
</TabPanel>
))}
</vertical-tabs>
</TabsState>
return styled(style)(
<Loader state={children} style={style}>{() => styled(style)(
<>
{folders.length > 0 ? (
<TabsState currentTabId={folderId} orientation='vertical' lazy onChange={openFolder}>
<vertical-tabs>
<TabList aria-label="Object folders">
{folders.map(folderId => (
<FolderTabRenderer key={folderId} nodeId={nodeId} folderId={folderId} style={tabStyles} />
))}
</TabList>
{folders.map(folderId => (
<TabPanel key={folderId} tabId={folderId}>
<FolderPanelRenderer key={folderId} nodeId={nodeId} folderId={folderId} style={tabStyles} />
</TabPanel>
))}
</vertical-tabs>
</TabsState>
) : (
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
)}
</>
)}
</Loader>
);
});
@@ -37,16 +37,6 @@ export const ObjectPropertiesPagePanel: ObjectPagePanelComponent = observer(func
tab,
}) {
const style = useStyles(viewerStyles);
const translate = useTranslate();
const { children, isLoading } = useChildren(tab.handlerState.objectId);
if (!children && isLoading()) {
return <Loader />;
}
if (!children) {
return <TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>;
}
return styled(style)(
<wrapper>
@@ -9,12 +9,12 @@
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { useChildren } from '@cloudbeaver/core-app';
import { Loader, TextPlaceholder } from '@cloudbeaver/core-blocks';
import { DBObjectResource, NavTreeResource } from '@cloudbeaver/core-app';
import { Loader, TextPlaceholder, useMapResource } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { css } from '@reshadow/react';
import { useObjectFolder } from '../../useObjectFolder';
import { ObjectChildrenPropertyTable } from './ObjectChildrenPropertyTable';
const styles = css`
@@ -22,6 +22,9 @@ const styles = css`
flex: auto;
overflow: hidden;
}
ExceptionMessage {
padding: 24px;
}
`;
interface ObjectPropertyTableProps {
@@ -34,21 +37,27 @@ export const ObjectPropertyTable = observer<ObjectPropertyTableProps>(function O
parentId,
}) {
const translate = useTranslate();
const children = useChildren(objectId);
const { isLoading } = useObjectFolder(objectId);
if ((!children.children && children.isLoading()) || isLoading) {
return <Loader />;
}
if (!children?.children || !children.children.length) {
return <TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>;
}
const tree = useMapResource(NavTreeResource, objectId);
const key = resourceKeyList([objectId, ...tree.data || []]);
const dbObject = useMapResource(DBObjectResource, key, {
async onLoad(resource: DBObjectResource) {
await resource.loadChildren(objectId, key);
},
});
return styled(styles)(
<div>
<ObjectChildrenPropertyTable nodeIds={children.children} />
</div>
<Loader state={[tree, dbObject]} style={styles}>{() => styled(styles)(
<>
{!tree.data || tree.data.length === 0 ? (
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
) : (
<div>
<ObjectChildrenPropertyTable nodeIds={tree.data} />
</div>
)}
</>
)}
</Loader>
);
});
@@ -6,13 +6,14 @@
* 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 { useCallback } from 'react';
import styled, { css } from 'reshadow';
import { useChildren, TabHandlerPanelComponent } from '@cloudbeaver/core-app';
import { Loader, TabsBox, TabPanel, TextPlaceholder, Button } from '@cloudbeaver/core-blocks';
import { useConnectionInfo } from '@cloudbeaver/core-connections';
import { TabHandlerPanelComponent, NavTreeResource, NavNodeInfoResource } from '@cloudbeaver/core-app';
import { Loader, TabsBox, TabPanel, TextPlaceholder, Button, useMapResource, useObservableRef } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource, ConnectionsManagerService } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles, composes } from '@cloudbeaver/core-theming';
@@ -35,6 +36,9 @@ const styles = composes(
tab-outer:only-child {
display: none;
}
ExceptionMessage {
padding: 24px;
}
`
);
@@ -42,26 +46,72 @@ export const ObjectViewerPanel: TabHandlerPanelComponent<IObjectViewerTabState>
tab,
}) {
const translate = useTranslate();
const [connecting, setConnecting] = useState(false);
const connection = useConnectionInfo(tab.handlerState.connectionId || '');
const style = useStyles(styles);
const {
children, isOutdated, isLoading, isLoaded,
} = useChildren(tab.handlerState.objectId);
const dbObjectPagesService = useService(DBObjectPageService);
const connectionsManagerService = useService(ConnectionsManagerService);
const navNodeInfoResource = useService(NavNodeInfoResource);
const state = useObservableRef(() => ({
connecting: false,
notFound: false,
}), {
connecting: observable.ref,
notFound: observable.ref,
}, false);
const connection = useMapResource(ConnectionInfoResource, tab.handlerState.connectionId || null, {
isActive: resource => !tab.handlerState.connectionId || !resource.has(tab.handlerState.connectionId),
});
const children = useMapResource(NavTreeResource, tab.handlerState.parentId, {
onLoad: async resource => {
if (tab.handlerState.parents.length === 0) {
return true;
}
const first = tab.handlerState.parents[0];
await resource.load(first);
for (const nodeId of tab.handlerState.parents) {
if (!navNodeInfoResource.has(nodeId)) {
state.notFound = true;
return true;
}
await resource.load(nodeId);
}
state.notFound = false;
return false;
},
isActive: () => connection.data?.connected || false,
});
const dataPreloaded = children.isLoaded() && !!children.data?.includes(tab.handlerState.objectId) && !state.notFound;
const node = useMapResource(navNodeInfoResource, dataPreloaded ? tab.handlerState.objectId : null, {
onData(data) {
tab.handlerState.tabIcon = data.icon;
tab.handlerState.tabTitle = data.name;
},
});
const pages = dbObjectPagesService.orderedPages;
const handleConnect = useCallback(async () => {
setConnecting(true);
try {
await connection.connect();
} finally {
setConnecting(false);
if (state.connecting || !connection.data) {
return;
}
}, [connection]);
if (connection.connectionInfo && !connection.connectionInfo.connected) {
if (connecting || connection.isLoading()) {
state.connecting = true;
try {
await connectionsManagerService.requireConnection(connection.data.id);
} finally {
state.connecting = false;
}
}, []);
if (connection.data && !connection.data.connected) {
if (state.connecting || connection.isLoading()) {
return <Loader />;
}
@@ -72,37 +122,39 @@ export const ObjectViewerPanel: TabHandlerPanelComponent<IObjectViewerTabState>
);
}
if (tab.handlerState.error) {
if (tab.handlerState.error || state.notFound) {
return <TextPlaceholder>{translate('plugin_object_viewer_error')}</TextPlaceholder>;
}
if (!isLoaded() || (!isOutdated() && isLoading())) {
return <Loader />;
}
if (!children) {
return <TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>;
}
return styled(style)(
<TabsBox
currentTabId={tab.handlerState.pageId}
tabs={pages.map(page => (
<DBObjectPageTab
key={page.key}
tab={tab}
page={page}
style={styles}
onSelect={dbObjectPagesService.selectPage}
/>
))}
style={styles}
>
{pages.map(page => (
<TabPanel key={page.key} tabId={page.key} lazy>
<DBObjectPagePanel tab={tab} page={page} />
</TabPanel>
))}
</TabsBox>
<Loader state={[node, children]} style={styles}>
{() => styled(style)(
<>
{node.data ? (
<TabsBox
currentTabId={tab.handlerState.pageId}
tabs={pages.map(page => (
<DBObjectPageTab
key={page.key}
tab={tab}
page={page}
style={styles}
onSelect={dbObjectPagesService.selectPage}
/>
))}
style={styles}
>
{pages.map(page => (
<TabPanel key={page.key} tabId={page.key} lazy>
<DBObjectPagePanel tab={tab} page={page} />
</TabPanel>
))}
</TabsBox>
) : (
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
)}
</>
)}
</Loader>
);
});
@@ -11,17 +11,15 @@ import {
INodeNavigationData,
ITab,
TabHandler,
NodeManagerUtils,
objectCatalogProvider,
objectSchemaProvider,
NavNodeManagerService,
DBObjectService
NavNodeManagerService
} from '@cloudbeaver/core-app';
import { connectionProvider, ConnectionInfoResource, Connection } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import type { IAsyncContextLoader, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import type { IObjectViewerTabContext } from './IObjectViewerTabContext';
import type { IObjectViewerTabState } from './IObjectViewerTabState';
@@ -37,7 +35,6 @@ export class ObjectViewerTabService {
constructor(
private navNodeManagerService: NavNodeManagerService,
private dbObjectService: DBObjectService,
private dbObjectPageService: DBObjectPageService,
private notificationService: NotificationService,
private navigationTabsService: NavigationTabsService,
@@ -291,51 +288,15 @@ export class ObjectViewerTabService {
}
}
for (const nodeId of tab.handlerState.parents) {
await this.navNodeManagerService.loadTree(nodeId);
}
// TODO: must be loaded by info folder?
const node = await this.navNodeManagerService.loadNode({
nodeId: tab.handlerState.objectId,
parentId: tab.handlerState.parentId,
});
const currentPage = this.dbObjectPageService.getPage(tab.handlerState.pageId);
if (currentPage) {
await this.dbObjectPageService.selectPage(tab, currentPage);
}
if (node) {
tab.handlerState.tabIcon = node.icon;
tab.handlerState.tabTitle = node.name;
}
await this.dbObjectService.load(tab.handlerState.objectId);
const children = await this.navNodeManagerService.loadTree(tab.handlerState.objectId);
if (tab.handlerState.childrenError) {
return;
}
try {
const folderId = tab.handlerState.folderId;
if (children.length === 0 || !NodeManagerUtils.isDatabaseObject(folderId)) {
await this.dbObjectService.loadChildren(tab.handlerState.objectId, resourceKeyList(children));
return;
}
const folderChildren = await this.navNodeManagerService.loadTree(folderId);
await this.dbObjectService.loadChildren(folderId, resourceKeyList(folderChildren));
} catch (exception) {
if (tab.handlerState.childrenError) {
return;
}
tab.handlerState.childrenError = true;
this.notificationService.logException(exception, 'Object Viewer Error', 'Error in Object Viewer while folder selecting');
}
} catch (exception) {
tab.handlerState.error = true;
this.notificationService.logException(exception, 'Object Viewer Error', 'Error in Object Viewer while tab selecting');
@@ -6,18 +6,18 @@
* you may not use this file except in compliance with the License.
*/
import { NavNodeManagerService, DBObjectService } from '@cloudbeaver/core-app';
import { NavNodeManagerService, DBObjectResource } from '@cloudbeaver/core-app';
import { useService } from '@cloudbeaver/core-di';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
export function useObjectFolder(objectId: string) {
const navNodeManagerService = useService(NavNodeManagerService);
const dbObjectService = useService(DBObjectService);
const dbObjectResource = useService(DBObjectResource);
const children = navNodeManagerService.getTree(objectId) || [];
const isLoading = !dbObjectService.isLoaded(resourceKeyList(children))
&& dbObjectService.isDataLoading(resourceKeyList(children));
const isLoading = !dbObjectResource.isLoaded(resourceKeyList(children))
&& dbObjectResource.isDataLoading(resourceKeyList(children));
return { isLoading };
}