From e659a64cbd0bba2fd55a70cfd39eaca8e080c110 Mon Sep 17 00:00:00 2001 From: Alexey Date: Tue, 4 Apr 2023 12:52:35 +0300 Subject: [PATCH] CB-3274 fix folders sync (#1568) * CB-3274 fix: connection / resource folders sync * CB-3274 fix: connection folders caching * CB-3274 fix: dialog error handling * CB-3274 fix: nav tree folders mode * CB-3274 fix: nav tree performance * CB-3263 fix: new folders validation * CB-3263 fix: commit includes to alias transformation * CB-3274 chore: clear code --- .../packages/core-app/src/AppScreen/Main.tsx | 4 +- .../packages/core-blocks/src/DisplayError.tsx | 18 +- .../core-blocks/src/ErrorBoundary.tsx | 11 +- .../core-blocks/src/ExceptionMessage.tsx | 59 ++- .../src/FolderExplorer/FolderExplorerPath.tsx | 7 +- .../FolderExplorer/folderExplorerStyles.ts | 5 +- .../src/FormControls/InputField.tsx | 18 +- .../src/ResourcesHooks/useResource.ts | 15 +- webapp/packages/core-blocks/src/useStyles.ts | 2 +- .../packages/core-blocks/src/useUserData.ts | 16 +- .../src/ConnectionFolderResource.ts | 54 +-- .../src/NavTree/ConnectionNavNodeService.ts | 10 +- .../getConnectionFolderIdFromNodeId.ts | 21 ++ ...lderParents.ts => getFolderNodeParents.ts} | 2 +- .../src/createConnectionFolderParam.ts | 31 ++ webapp/packages/core-connections/src/index.ts | 5 + .../src/CommonDialog/CommonDialog/styles.ts | 343 +++++++++--------- .../src/CommonDialog/CommonDialogService.ts | 3 + .../src/CommonDialog/DialogsPortal.tsx | 24 +- .../core-dialogs/src/CommonDialog/styles.ts | 4 + .../src/NodesManager/NavTreeResource.ts | 5 +- .../src/Resource/CachedMapResource.ts | 6 +- .../core-sdk/src/Resource/CachedResource.ts | 24 +- .../core-sdk/src/Resource/ResourceError.ts | 2 +- .../src/LocalStorageSaveService.ts | 11 +- .../core-theming/src/ComponentStyle.ts | 2 +- webapp/packages/core-utils/src/MetadataMap.ts | 102 +++--- webapp/packages/core-utils/src/TempMap.ts | 49 ++- webapp/packages/core-utils/src/cacheValue.ts | 34 ++ webapp/packages/core-utils/src/index.ts | 2 + webapp/packages/core-utils/src/isPrimitive.ts | 11 + .../NavNodes/ConnectionFoldersBootstrap.ts | 60 +-- .../plugin-devtools/src/DevToolsService.ts | 23 ++ .../plugin-devtools/src/PluginBootstrap.ts | 11 + .../ACTION_DEVTOOLS_MODE_DISTRIBUTED.ts | 15 + .../NavigationTabs/NavigationTabsService.ts | 2 +- .../ElementsTree/ElementsTree.tsx | 93 +---- .../ElementsTreeContentLoader.tsx | 44 +-- .../ElementsTree/ElementsTreeContext.ts | 3 - .../ElementsTreeTools/ElementsTreeTools.tsx | 4 +- .../NavigationNode/NavigationNodeNested.tsx | 1 - .../NavigationTreeNode/useNavigationNode.ts | 4 +- .../ElementsTree/useElementsTree.ts | 218 +++++++---- .../useElementsTreeFolderExplorer.ts | 32 ++ .../src/NavigationTree/NavigationTree.tsx | 5 - .../NavigationTree/NavigationTreeService.ts | 11 +- .../NavigationNodeProjectControl.tsx | 5 + .../src/ObjectPage/ObjectPage.ts | 4 +- .../plugin-projects/src/FolderDialog.tsx | 48 ++- .../src/NavNodes/ResourceFoldersBootstrap.ts | 90 ++--- webapp/readme.md | 23 +- 51 files changed, 960 insertions(+), 636 deletions(-) create mode 100644 webapp/packages/core-connections/src/NavTree/getConnectionFolderIdFromNodeId.ts rename webapp/packages/core-connections/src/NavTree/{getFolderParents.ts => getFolderNodeParents.ts} (84%) create mode 100644 webapp/packages/core-connections/src/createConnectionFolderParam.ts create mode 100644 webapp/packages/core-utils/src/cacheValue.ts create mode 100644 webapp/packages/core-utils/src/isPrimitive.ts create mode 100644 webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED.ts create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTreeFolderExplorer.ts diff --git a/webapp/packages/core-app/src/AppScreen/Main.tsx b/webapp/packages/core-app/src/AppScreen/Main.tsx index f4f0d30dea..ca42bf4043 100644 --- a/webapp/packages/core-app/src/AppScreen/Main.tsx +++ b/webapp/packages/core-app/src/AppScreen/Main.tsx @@ -50,7 +50,7 @@ export const Main = observer(function Main() { mode={leftBarDisabled ? 'minimize' : splitMainState.mode} disable={leftBarDisabled} > - + @@ -67,7 +67,7 @@ export const Main = observer(function Main() { - + diff --git a/webapp/packages/core-blocks/src/DisplayError.tsx b/webapp/packages/core-blocks/src/DisplayError.tsx index 3baaa02028..08cf3c6338 100644 --- a/webapp/packages/core-blocks/src/DisplayError.tsx +++ b/webapp/packages/core-blocks/src/DisplayError.tsx @@ -10,12 +10,14 @@ import type React from 'react'; import styled, { css, use } from 'reshadow'; import { ENotificationType } from '@cloudbeaver/core-events'; +import type { ComponentStyle } from '@cloudbeaver/core-theming'; import { AppRefreshButton } from './AppRefreshButton'; import { NotificationMark } from './Snackbars/NotificationMark'; +import { useStyles } from './useStyles'; const style = css` - container { + error { width: 100%; height: 100%; display: flex; @@ -25,7 +27,7 @@ const style = css` height: 100vh; } } - container-inner-block { + error-inner-block { display: flex; margin: auto; padding: 16px 24px; @@ -46,6 +48,7 @@ interface Props { error?: Error; errorInfo?: React.ErrorInfo; className?: string; + styles?: ComponentStyle; } export const DisplayError: React.FC> = function DisplayError({ @@ -54,12 +57,13 @@ export const DisplayError: React.FC> = function D error, errorInfo, className, + styles, }) { const stack = errorInfo?.componentStack || error?.stack; - return styled(style)( - - + return styled(useStyles(style, styles))( + +

Something went wrong.

{root && } @@ -71,7 +75,7 @@ export const DisplayError: React.FC> = function D {stack} )} -
-
+ + ); }; diff --git a/webapp/packages/core-blocks/src/ErrorBoundary.tsx b/webapp/packages/core-blocks/src/ErrorBoundary.tsx index 010a3868b4..973842c2e8 100644 --- a/webapp/packages/core-blocks/src/ErrorBoundary.tsx +++ b/webapp/packages/core-blocks/src/ErrorBoundary.tsx @@ -9,6 +9,7 @@ import React, { ErrorInfo } from 'react'; import styled, { css } from 'reshadow'; +import type { ComponentStyle } from '@cloudbeaver/core-theming'; import { errorOf, LoadingError } from '@cloudbeaver/core-utils'; import { Button } from './Button'; @@ -23,12 +24,14 @@ const style = css` `; interface Props { - onRefresh?: () => any; icon?: boolean; root?: boolean; inline?: boolean; remount?: boolean; className?: string; + styles?: ComponentStyle; + onClose?: () => any; + onRefresh?: () => any; } interface IErrorData { @@ -76,7 +79,7 @@ export class ErrorBoundary } render(): React.ReactNode { - const { root, inline, icon, children, className } = this.props; + const { root, inline, icon, children, styles, className, onClose } = this.props; for (const errorData of this.state.exceptions) { if (root) { @@ -85,8 +88,10 @@ export class ErrorBoundary className={className} root={root} error={errorData.error} + styles={styles} errorInfo={errorData.errorInfo} > + {onClose && } {this.canRefresh && } ); @@ -97,7 +102,9 @@ export class ErrorBoundary icon={icon} className={className} exception={errorData.error} + styles={styles} onRetry={this.canRefresh ? this.refresh : undefined} + onClose={onClose} /> ); } diff --git a/webapp/packages/core-blocks/src/ExceptionMessage.tsx b/webapp/packages/core-blocks/src/ExceptionMessage.tsx index b6c6a6f89d..3fbf4ca488 100644 --- a/webapp/packages/core-blocks/src/ExceptionMessage.tsx +++ b/webapp/packages/core-blocks/src/ExceptionMessage.tsx @@ -9,12 +9,16 @@ import { observer } from 'mobx-react-lite'; import styled, { css, use } from 'reshadow'; +import type { ComponentStyle } from '@cloudbeaver/core-theming'; + import { Button } from './Button'; +import { Icon } from './Icon'; import { IconOrImage } from './IconOrImage'; import { useTranslate } from './localization/useTranslate'; import { useErrorDetails } from './useErrorDetails'; +import { useStyles } from './useStyles'; -const styles = css` +const style = css` error { flex: 1; display: flex; @@ -91,6 +95,12 @@ const styles = css` error[|icon] { padding: 0; } + error-action-close { + cursor: pointer; + width: 18px; + height: 18px; + flex-shrink: 0; + } `; interface Props { @@ -100,11 +110,13 @@ interface Props { icon?: boolean; inline?: boolean; className?: string; + styles?: ComponentStyle; onRetry?: () => void; + onClose?: () => void; } export const ExceptionMessage = observer(function ExceptionMessage({ - name, message, exception = null, icon, inline, className, onRetry, + name, message, exception = null, icon, inline, className, styles, onRetry, onClose, }) { const translate = useTranslate(); const error = useErrorDetails(exception); @@ -120,28 +132,37 @@ export const ExceptionMessage = observer(function ExceptionMessage({ }; } - return styled(styles)( + return styled(useStyles(style, styles))( {!icon && ( - - {name} - {message} - - {error.hasDetails && ( - - )} - {onRetry && ( - - )} - - + <> + + + {name} + + {message} + + {error.hasDetails && ( + + )} + {onRetry && ( + + )} + + + {onClose && ( + + + + )} + )} ); diff --git a/webapp/packages/core-blocks/src/FolderExplorer/FolderExplorerPath.tsx b/webapp/packages/core-blocks/src/FolderExplorer/FolderExplorerPath.tsx index cc30f927f1..8f0daae043 100644 --- a/webapp/packages/core-blocks/src/FolderExplorer/FolderExplorerPath.tsx +++ b/webapp/packages/core-blocks/src/FolderExplorer/FolderExplorerPath.tsx @@ -44,7 +44,12 @@ export const FolderExplorerPath = observer(function FolderExplorerPath({ const path = context.state.fullPath.slice(0, i); const skipFolder = !canSkip || canSkip(folder); - if (i === 0 || i === context.state.fullPath.length - 1 || !skipFolder || context.state.fullPath.length < 5) { + if ( + i === 0 + || i === context.state.fullPath.length - 1 + || !skipFolder + || context.state.fullPath.length < 5 + ) { if (skip) { pathElements.push( , 'onChange' | 'name' | 'value' | 'style'> & ILayoutSizeProps & { error?: boolean; + loading?: boolean; description?: string; labelTooltip?: string; mod?: 'surface'; @@ -107,6 +112,7 @@ export const InputField: InputFieldType = observer(forwardRef(function InputFiel children, className, error, + loading, description, labelTooltip, mod, @@ -130,6 +136,7 @@ export const InputField: InputFieldType = observer(forwardRef(function InputFiel style ); const context = useContext(FormContext); + loading = useStateDelay(loading ?? false, 300); const revealPassword = useCallback(() => { if (rest.disabled) { @@ -192,6 +199,13 @@ export const InputField: InputFieldType = observer(forwardRef(function InputFiel {...use({ mod })} required={required} /> + {loading && ( + + + + )} {showRevealPasswordButton && ( { } type ResourceData< - TResource extends CachedResource, + TResource extends CachedResource, TKey, TIncludes -> = TResource extends CachedDataResource +> = TResource extends CachedDataResource ? CachedResourceData : CachedMapResourceLoader< TKey, @@ -39,11 +39,12 @@ type ResourceData< ; interface IActions< - TResource extends CachedResource, + TResource extends CachedResource, TKey, TIncludes > { active?: boolean; + forceSuspense?: boolean; silent?: boolean; onData?: ( data: ResourceData, @@ -110,7 +111,7 @@ type TResult = ( ); export function useResource< - TResource extends CachedResource, + TResource extends CachedResource, TKeyArg extends ResourceKey>, TIncludes extends Readonly> >( @@ -121,7 +122,7 @@ export function useResource< ): TResult; export function useResource< - TResource extends CachedResource, + TResource extends CachedResource, TKeyArg extends ResourceKey>, TIncludes extends CachedResourceContext >( @@ -468,6 +469,10 @@ export function useResource< } }, [result.canLoad]); + if (actions?.forceSuspense) { + result.data; + } + return result; } diff --git a/webapp/packages/core-blocks/src/useStyles.ts b/webapp/packages/core-blocks/src/useStyles.ts index 2e756fe1b2..d8508a9256 100644 --- a/webapp/packages/core-blocks/src/useStyles.ts +++ b/webapp/packages/core-blocks/src/useStyles.ts @@ -82,5 +82,5 @@ export function useStyles( } export function joinStyles(...styles: ComponentStyle[]): ComponentStyle { - return styles.flat(); + return styles.flat(2); } diff --git a/webapp/packages/core-blocks/src/useUserData.ts b/webapp/packages/core-blocks/src/useUserData.ts index bf48f90c6d..e2b6073a70 100644 --- a/webapp/packages/core-blocks/src/useUserData.ts +++ b/webapp/packages/core-blocks/src/useUserData.ts @@ -8,26 +8,30 @@ import { useEffect, useRef } from 'react'; -import { UserDataService } from '@cloudbeaver/core-authentication'; +import { UserDataService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { useService } from '@cloudbeaver/core-di'; +import { useResource } from './ResourcesHooks/useResource'; +import { useObjectRef } from './useObjectRef'; + export function useUserData>( key: string, defaultValue: () => T, onUpdate?: (data: T) => void, validate?: (data: T) => boolean ): T { + const optionsRef = useObjectRef({ defaultValue, onUpdate, validate }); + useResource(useUserData, UserInfoResource, undefined); const userDataService = useService(UserDataService); const ref = useRef(null); const data = userDataService.getUserData(key, defaultValue, validate); - const previous = ref.current; - ref.current = data; useEffect(() => { - if (previous !== data) { - onUpdate?.(data); + if (ref.current !== data) { + ref.current = data; + optionsRef.onUpdate?.(data); } - }); + }, [ref.current, data]); return data; } diff --git a/webapp/packages/core-connections/src/ConnectionFolderResource.ts b/webapp/packages/core-connections/src/ConnectionFolderResource.ts index a7fb8eb82d..05ca6e911f 100644 --- a/webapp/packages/core-connections/src/ConnectionFolderResource.ts +++ b/webapp/packages/core-connections/src/ConnectionFolderResource.ts @@ -11,7 +11,10 @@ import { action, makeObservable, runInAction } from 'mobx'; import { AppAuthService } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { SessionDataResource } from '@cloudbeaver/core-root'; -import { GraphQLService, CachedMapResource, resourceKeyList, ResourceKeyUtils, CachedMapAllKey, ConnectionFolderInfoFragment, isResourceKeyList, isResourceKeyAlias, resourceKeyAliasFactory, ResourceKey, isResourceAlias } from '@cloudbeaver/core-sdk'; +import { GraphQLService, CachedMapResource, resourceKeyList, ResourceKeyUtils, CachedMapAllKey, ConnectionFolderInfoFragment, resourceKeyAliasFactory, ResourceKey, isResourceAlias } from '@cloudbeaver/core-sdk'; + +import { createConnectionFolderParam } from './createConnectionFolderParam'; +import { getConnectionFolderIdFromNodeId } from './NavTree/getConnectionFolderIdFromNodeId'; export type ConnectionFolder = ConnectionFolderInfoFragment; @@ -49,13 +52,14 @@ export class ConnectionFolderResource extends CachedMapResource { + async create(projectId: string, name: string, parentId?: string): Promise { const { folder } = await this.graphQLService.sdk.createConnectionFolder({ - projectId: key.projectId, - folderName: key.folderId, + projectId, + folderName: name, parentFolderPath: parentId, }); + const key = createConnectionFolderParam(projectId, folder.id); this.set(key, { ...folder, projectId: key.projectId }); return this.get(key)!; @@ -72,12 +76,10 @@ export class ConnectionFolderResource extends CachedMapResource> { const all = this.isAlias(originalKey, CachedMapAllKey); const isProjectFolders = this.isAlias(originalKey, ConnectionFolderProjectKey); - const folders: ConnectionFolder[] = []; + const folderList: ConnectionFolder[] = []; let projectId: string | undefined; let folderId: string | undefined; @@ -108,21 +110,21 @@ export class ConnectionFolderResource extends CachedMapResource(folder => createConnectionFolderParam( + const key = resourceKeyList(folderList.map(folder => createConnectionFolderParam( folder.projectId, folder.id, ))); runInAction(() => { if (all) { - this.replace(key, folders); + this.replace(key, folderList); } else { if (isProjectFolders) { const removedFolders = this.keys - .filter(key => !folders.some(f => ( + .filter(key => !folderList.some(f => ( key.projectId === projectId && key.folderId === f.id ))); @@ -130,7 +132,7 @@ export class ConnectionFolderResource extends CachedMapResource { const parents = getFolderNodeParents(nodeId); - return parents[parents.length - 2]; + return parents[parents.length - 1]; }); - this.navTreeResource.markTreeOutdated(resourceKeyList(parents)); + this.navTreeResource.markOutdated(resourceKeyList(parents)); }, undefined, this.navTreeResource @@ -70,10 +70,10 @@ export class ConnectionNavNodeService extends Dependency { const parents = data.nodePaths.map(nodeId => { const parents = getFolderNodeParents(nodeId); - return parents[parents.length - 2]; + return parents[parents.length - 1]; }); - this.navTreeResource.deleteInNode(resourceKeyList(parents), data.nodePaths); + this.navTreeResource.deleteInNode(resourceKeyList(parents), data.nodePaths.map(value => [value])); }, undefined, this.navTreeResource diff --git a/webapp/packages/core-connections/src/NavTree/getConnectionFolderIdFromNodeId.ts b/webapp/packages/core-connections/src/NavTree/getConnectionFolderIdFromNodeId.ts new file mode 100644 index 0000000000..2d09b682bc --- /dev/null +++ b/webapp/packages/core-connections/src/NavTree/getConnectionFolderIdFromNodeId.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { IConnectionFolderParam } from '../ConnectionFolderResource'; +import { createConnectionFolderParam } from '../createConnectionFolderParam'; + +export function getConnectionFolderIdFromNodeId(nodeId: string): IConnectionFolderParam | undefined { + const data = /^folder:\/\/(.*?)\/(.*)$/ig.exec(nodeId); + + if (data) { + const [t, projectId, folderId] = data; + return createConnectionFolderParam(projectId, folderId); + } + + return undefined; +} \ No newline at end of file diff --git a/webapp/packages/core-connections/src/NavTree/getFolderParents.ts b/webapp/packages/core-connections/src/NavTree/getFolderNodeParents.ts similarity index 84% rename from webapp/packages/core-connections/src/NavTree/getFolderParents.ts rename to webapp/packages/core-connections/src/NavTree/getFolderNodeParents.ts index 557e6aa0dc..5f5e1e7f29 100644 --- a/webapp/packages/core-connections/src/NavTree/getFolderParents.ts +++ b/webapp/packages/core-connections/src/NavTree/getFolderNodeParents.ts @@ -15,7 +15,7 @@ export function getFolderNodeParents(nodeId: string): string[] { if (isFolderNodeId(nodeId)) { const parents = getPathParents(nodeId); - return [getProjectNodeId(parents[0].replace('folder://', '')), ...parents.slice(1)]; + return [parents[0], getProjectNodeId(parents[1].replace('folder://', '')), ...parents.slice(2)]; } return []; diff --git a/webapp/packages/core-connections/src/createConnectionFolderParam.ts b/webapp/packages/core-connections/src/createConnectionFolderParam.ts new file mode 100644 index 0000000000..57eecbccc6 --- /dev/null +++ b/webapp/packages/core-connections/src/createConnectionFolderParam.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { ConnectionFolder, IConnectionFolderParam } from './ConnectionFolderResource'; + +export function createConnectionFolderParam( + projectId: string, + folder: ConnectionFolder +): IConnectionFolderParam; +export function createConnectionFolderParam( + projectId: string, + folderId: string +): IConnectionFolderParam; +export function createConnectionFolderParam( + projectId: string, + folderIdOrFolder: string | ConnectionFolder +): IConnectionFolderParam { + if (typeof folderIdOrFolder === 'object') { + folderIdOrFolder = folderIdOrFolder.id; + } + + return { + projectId: projectId, + folderId: folderIdOrFolder, + }; +} \ No newline at end of file diff --git a/webapp/packages/core-connections/src/index.ts b/webapp/packages/core-connections/src/index.ts index 1ce2dc4ff3..66fd35c403 100644 --- a/webapp/packages/core-connections/src/index.ts +++ b/webapp/packages/core-connections/src/index.ts @@ -10,6 +10,10 @@ export * from './extensions/IObjectSchemaProvider'; export * from './extensions/IObjectSchemaSetter'; export * from './NavTree/ConnectionNavNodeService'; export * from './NavTree/NavNodeExtensionsService'; +export * from './NavTree/getConnectionFolderIdFromNodeId'; +export * from './NavTree/getConnectionFolderId'; +export * from './NavTree/getConnectionParentId'; +export * from './NavTree/getFolderNodeParents'; export * from './extensions/IConnectionProvider'; export * from './extensions/IConnectionSetter'; @@ -22,6 +26,7 @@ export * from './EConnectionFeature'; export * from './ConnectionsSettingsService'; export * from './ContainerResource'; export * from './ConnectionsLocaleService'; +export * from './createConnectionFolderParam'; export * from './DatabaseAuthModelsResource'; export * from './DatabaseConnection'; export * from './DBDriverResource'; diff --git a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/styles.ts b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/styles.ts index 387787d577..e3329803c9 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/styles.ts +++ b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/styles.ts @@ -25,176 +25,181 @@ export const commonDialogThemeStyle: ThemeSelector = async theme => { return [styles.default]; }; -export const commonDialogBaseStyle = css` - dialog { - composes: theme-background-surface theme-text-on-surface theme-elevation-z10 from global; - display: flex; - flex-direction: column; - position: relative; - overflow: hidden; - margin: 0; - border: none; - height: auto; - max-height: 100%; - max-width: 748px; - border-radius: 0.25rem; +export const commonDialogContainerStyles = css` + error, dialog { + composes: theme-background-surface theme-text-on-surface theme-elevation-z10 from global; + border-radius: 0.25rem; + } +`; + +export const commonDialogBaseStyle = [commonDialogContainerStyles, css` + dialog { + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + margin: 0; + border: none; + height: auto; + max-height: 100%; + max-width: 748px; + padding: 0px; + + &[|size=small] { + min-width: 404px; + min-height: 262px; + max-height: max(100vh - 48px, 262px); + + &[|fixedSize] { + width: 404px; + height: 262px; + } + &[|fixedWidth] { + width: 404px; + } + } + &[|size=medium] { + min-width: 576px; + min-height: 374px; + max-height: max(100vh - 48px, 374px); + + &[|fixedSize] { + width: 576px; + height: 374px; + } + &[|fixedWidth] { + width: 576px; + } + } + &[|size=large] { + min-width: 720px; + min-height: 468px; + max-height: max(100vh - 48px, 468px); + + &[|fixedSize] { + width: 720px; + height: 468px; + } + &[|fixedWidth] { + width: 720px; + } + } + } + header, dialog-body, footer { + flex-shrink: 0; + padding: 24px; + + &[|no-padding] { padding: 0px; - - &[|size=small] { - min-width: 404px; - min-height: 262px; - max-height: max(100vh - 48px, 262px); - - &[|fixedSize] { - width: 404px; - height: 262px; - } - &[|fixedWidth] { - width: 404px; - } - } - &[|size=medium] { - min-width: 576px; - min-height: 374px; - max-height: max(100vh - 48px, 374px); - - &[|fixedSize] { - width: 576px; - height: 374px; - } - &[|fixedWidth] { - width: 576px; - } - } - &[|size=large] { - min-width: 720px; - min-height: 468px; - max-height: max(100vh - 48px, 468px); - - &[|fixedSize] { - width: 720px; - height: 468px; - } - &[|fixedWidth] { - width: 720px; - } - } } - header, dialog-body, footer { - flex-shrink: 0; - padding: 24px; - - &[|no-padding] { - padding: 0px; - } + } + dialog-body { + padding-top: 0px; + padding-right: 0px; + } + footer { + padding-top: 0px; + } + header { + position: relative; + display: grid; + grid-template-columns: max-content 1fr; + } + header-title { + display: flex; + align-items: center; + justify-content: space-between; + position: relative; + min-height: 24px; + overflow: hidden; + } + icon-container { + display: flex; + align-items: center; + justify-content: center; + } + IconOrImage { + width: 24px; + height: 24px; + margin-right: 16px; + &[|bigIcon] { + width: 40px; + height: 40px; } - dialog-body { - padding-top: 0px; - padding-right: 0px; + } + dialog-body { + flex: 1; + box-sizing: content-box; + display: flex; + max-height: 100%; + overflow: auto; + } + dialog-body-overflow-box { + position: relative; + flex: 1; + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: auto; + word-break: break-word; + white-space: pre-wrap; + padding-right: 24px; + } + dialog-body-content { + flex: 1; + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; + } + Loader { + height: 100%; + } + dialog-body[|no-padding] + footer { + padding-top: 24px; + } + dialog-body[|no-padding] dialog-body-overflow-box { + padding-right: 0; + } + dialog-body[|no-overflow] dialog-body-content { + overflow: auto; + } + dialog-body-overflow { + composes: branding-overflow from global; + position: sticky; + bottom: 0; + left: 0; + flex-shrink: 0; + width: 100%; + height: 24px; + pointer-events: none; + } + h3 { + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + sub-title { + composes: theme-typography--caption from global; + grid-column: 2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + reject { + cursor: pointer; + width: 18px; + height: 18px; + flex-shrink: 0; + } + footer { + display: flex; + z-index: 0; + box-sizing: border-box; + + &:empty { + display: none; } - footer { - padding-top: 0px; - } - header { - position: relative; - display: grid; - grid-template-columns: max-content 1fr; - } - header-title { - display: flex; - align-items: center; - justify-content: space-between; - position: relative; - min-height: 24px; - overflow: hidden; - } - icon-container { - display: flex; - align-items: center; - justify-content: center; - } - IconOrImage { - width: 24px; - height: 24px; - margin-right: 16px; - &[|bigIcon] { - width: 40px; - height: 40px; - } - } - dialog-body { - flex: 1; - box-sizing: content-box; - display: flex; - max-height: 100%; - overflow: auto; - } - dialog-body-overflow-box { - position: relative; - flex: 1; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow: auto; - word-break: break-word; - white-space: pre-wrap; - padding-right: 24px; - } - dialog-body-content { - flex: 1; - position: relative; - display: flex; - flex-direction: column; - box-sizing: border-box; - } - Loader { - height: 100%; - } - dialog-body[|no-padding] + footer { - padding-top: 24px; - } - dialog-body[|no-padding] dialog-body-overflow-box { - padding-right: 0; - } - dialog-body[|no-overflow] dialog-body-content { - overflow: auto; - } - dialog-body-overflow { - composes: branding-overflow from global; - position: sticky; - bottom: 0; - left: 0; - flex-shrink: 0; - width: 100%; - height: 24px; - pointer-events: none; - } - h3 { - margin: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - sub-title { - composes: theme-typography--caption from global; - grid-column: 2; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - reject { - cursor: pointer; - width: 18px; - height: 18px; - flex-shrink: 0; - } - footer { - display: flex; - z-index: 0; - box-sizing: border-box; - - &:empty { - display: none; - } - } - `; + } +`]; diff --git a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialogService.ts b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialogService.ts index 85ba54505a..30a44889cc 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialogService.ts +++ b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialogService.ts @@ -9,6 +9,7 @@ import { observable, runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; +import { uuid } from '@cloudbeaver/core-utils'; export interface DialogOptions { persistent?: boolean; @@ -33,6 +34,7 @@ export enum DialogueStateResult { } export interface DialogInternal { + id: string; component: DialogComponent; payload: any; options?: DialogOptions; @@ -59,6 +61,7 @@ export class CommonDialogService { }); const dialogInternal: DialogInternal = { + id: uuid(), component, payload, resolve: _resolve!, diff --git a/webapp/packages/core-dialogs/src/CommonDialog/DialogsPortal.tsx b/webapp/packages/core-dialogs/src/CommonDialog/DialogsPortal.tsx index dbc69b2814..214640fb1e 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/DialogsPortal.tsx +++ b/webapp/packages/core-dialogs/src/CommonDialog/DialogsPortal.tsx @@ -11,10 +11,11 @@ import { useMemo } from 'react'; import { DialogBackdrop } from 'reakit/Dialog'; import styled from 'reshadow'; -import { Loader, useObjectRef, useStyles } from '@cloudbeaver/core-blocks'; +import { ErrorBoundary, Loader, useObjectRef, useStyles } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { commonDialogContainerStyles } from './CommonDialog/styles'; import { CommonDialogService, DialogInternal } from './CommonDialogService'; import { DialogContext, IDialogContext } from './DialogContext'; import { dialogStyles } from './styles'; @@ -54,13 +55,20 @@ export const DialogsPortal = observer(function DialogsPortal() { {commonDialogService.dialogs.map((dialog, i, arr) => ( - + + + ))} diff --git a/webapp/packages/core-dialogs/src/CommonDialog/styles.ts b/webapp/packages/core-dialogs/src/CommonDialog/styles.ts index 3a9b7e481b..69de67f1c9 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/styles.ts +++ b/webapp/packages/core-dialogs/src/CommonDialog/styles.ts @@ -30,6 +30,10 @@ export const dialogStyles = css` padding: 24px; flex-direction: column; align-items: center; + + & > :not(:last-child) { + display: none; + } } Dialog { diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index d0b6694238..14f15be6a7 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -411,10 +411,11 @@ export class NavTreeResource extends CachedMapResource `"${parent}"`).join('\n')}`); const error = new ResourceError(this, key, undefined, 'Entity not found', { cause }); ExecutorInterrupter.interrupt(contexts); throw this.markError(error, key); diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index 56eed3e663..cb943edcdd 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -20,10 +20,12 @@ import { ResourceKeyListAlias, resourceKeyListAlias } from './ResourceKeyListAli import { ResourceKeyUtils } from './ResourceKeyUtils'; export type CachedMapResourceKey = CachedResourceKey; -export type CachedMapResourceValue = TResource extends CachedResource, any, any, any> +export type CachedMapResourceValue = TResource extends CachedResource, any, any, any, any> + ? T + : never; +export type CachedMapResourceArguments = TResource extends CachedMapResource ? T : never; -export type CachedMapResourceArguments = TResource extends CachedMapResource ? T : never; export type CachedMapResourceListGetter< TValue, diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index 5c37fffdc2..e55b9df991 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -10,7 +10,7 @@ import { observable, makeObservable, action, computed, toJS } from 'mobx'; import { Dependency } from '@cloudbeaver/core-di'; import { ExecutionContext, Executor, ExecutorInterrupter, IExecutor, IExecutorHandler, IExecutionContextProvider, ISyncExecutor, SyncExecutor, TaskScheduler } from '@cloudbeaver/core-executor'; -import { MetadataMap, uuid } from '@cloudbeaver/core-utils'; +import { isPrimitive, MetadataMap, uuid } from '@cloudbeaver/core-utils'; import { isResourceAlias, ResourceAlias, ResourceAliasFactory, ResourceAliasOptions } from './ResourceAlias'; import { ResourceError } from './ResourceError'; @@ -560,6 +560,16 @@ export abstract class CachedResource< this.commitIncludes(metadata, includes); } }); + if (isResourceAlias(param)) { + param = this.transformToKey(param); + + this.updateMetadata(param, metadata => { + metadata.loaded = true; + if (includes) { + this.commitIncludes(metadata, includes); + } + }); + } } markError(exception: Error, key: ResourceKey, include?: TInclude): ResourceError { @@ -744,6 +754,9 @@ export abstract class CachedResource< * Can be override to provide static link to complicated keys */ protected getKeyRef(key: TKey): TKey { + if (isPrimitive(key)) { + return key; + } return Object.freeze(toJS(key)); } @@ -751,11 +764,15 @@ export abstract class CachedResource< * Can be override to provide static link to complicated keys */ protected getMetadataKeyRef(key: ResourceKeyFlat): TKey { - if (this.isAlias(key)) { + if (isResourceAlias(key)) { return this.transformToAlias(key) .toString() as TKey; } + if (isPrimitive(key)) { + return key; + } + const ref = Array.from(this.metadata.keys()) .find(k => this.isKeyEqual(k, key)); @@ -879,6 +896,9 @@ export abstract class CachedResource< } protected markOutdatedSync(key: ResourceKey): void { + if (!this.hasMetadata(key)) { + return; + } this.updateMetadata(key, metadata => { metadata.outdated = true; }); diff --git a/webapp/packages/core-sdk/src/Resource/ResourceError.ts b/webapp/packages/core-sdk/src/Resource/ResourceError.ts index 06e6c1e509..170b00902e 100644 --- a/webapp/packages/core-sdk/src/Resource/ResourceError.ts +++ b/webapp/packages/core-sdk/src/Resource/ResourceError.ts @@ -12,7 +12,7 @@ import type { CachedResource } from './CachedResource'; export class ResourceError extends LoadingError { constructor( - private readonly resource: CachedResource, + private readonly resource: CachedResource, private readonly key: any, private readonly context: any, message?: string, diff --git a/webapp/packages/core-settings/src/LocalStorageSaveService.ts b/webapp/packages/core-settings/src/LocalStorageSaveService.ts index 7dca00d42f..e7429a82a3 100644 --- a/webapp/packages/core-settings/src/LocalStorageSaveService.ts +++ b/webapp/packages/core-settings/src/LocalStorageSaveService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { action, autorun, entries, IReactionDisposer, keys, observable, ObservableMap, remove, runInAction, set, toJS } from 'mobx'; +import { entries, IReactionDisposer, keys, observable, ObservableMap, reaction, remove, runInAction, set, toJS } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; @@ -213,7 +213,14 @@ class DataStorage | Map> implements ILocalS subscribe() { this.unsubscribe(); - this.mobxSub = autorun(() => this.saveState()); + this.mobxSub = reaction( + () => toJS(this.store), + () => this.saveState(), + { + fireImmediately: true, + delay: 500, + } + ); } unsubscribe() { diff --git a/webapp/packages/core-theming/src/ComponentStyle.ts b/webapp/packages/core-theming/src/ComponentStyle.ts index 1361a6f504..dbf4b03287 100644 --- a/webapp/packages/core-theming/src/ComponentStyle.ts +++ b/webapp/packages/core-theming/src/ComponentStyle.ts @@ -12,4 +12,4 @@ export type BaseStyles = ClassCollection; export type ThemeSelector = (theme: string) => Promise; export type Style = BaseStyles | ThemeSelector; export type DynamicStyle = Style | boolean | undefined; -export type ComponentStyle = DynamicStyle | DynamicStyle[]; \ No newline at end of file +export type ComponentStyle = DynamicStyle | DynamicStyle[] | DynamicStyle[][]; \ No newline at end of file diff --git a/webapp/packages/core-utils/src/MetadataMap.ts b/webapp/packages/core-utils/src/MetadataMap.ts index 973075f18b..45870451c3 100644 --- a/webapp/packages/core-utils/src/MetadataMap.ts +++ b/webapp/packages/core-utils/src/MetadataMap.ts @@ -6,95 +6,99 @@ * you may not use this file except in compliance with the License. */ -import { makeObservable, observable, untracked } from 'mobx'; +import { observable, makeAutoObservable, action } from 'mobx'; + +import { TempMap } from './TempMap'; export type MetadataValueGetter = (key: TKey, metadata: MetadataMap) => TValue; export type DefaultValueGetter = (key: TKey, metadata: MetadataMap) => TValue; -export class MetadataMap { - private data: Map; - private length: number; - +export class MetadataMap implements Map { + get size(): number { + return this.temp.size; + } private syncData: Array<[TKey, TValue]> | null; + private readonly temp: TempMap; + private readonly data: Map; constructor(private readonly defaultValueGetter?: DefaultValueGetter) { - this.data = observable(new Map()); - this.length = 0; this.syncData = null; + this.data = observable(new Map()); + this.temp = new TempMap( + this.data, + () => { + this.syncData?.splice(0, this.syncData.length, ...this.data.entries()); + } + ); - makeObservable(this, { - data: observable.ref, + makeAutoObservable(this, { + sync: action, }); } - [Symbol.iterator]() { - return this.data[Symbol.iterator](); + [Symbol.iterator](): IterableIterator<[TKey, TValue]> { + return this.temp[Symbol.iterator](); + } + + get [Symbol.toStringTag](): string { + return 'MetadataMap'; } sync(entities: Array<[TKey, TValue]>): void { - this.data = observable(new Map(entities)); + this.temp.clear(); + this.data.clear(); + for (const [key, value] of entities) { + this.data.set(key, value); + } this.syncData = entities; } + forEach(callbackfn: (value: TValue, key: TKey, map: Map) => void, thisArg?: any): void { + this.temp.forEach(callbackfn, thisArg); + } + entries(): IterableIterator<[TKey, TValue]> { - return this.data.entries(); + return this.temp.entries(); } keys(): IterableIterator { - return this.data.keys(); + return this.temp.keys(); } values(): IterableIterator { - return this.data.values(); - } - - count(): number { - return this.length; + return this.temp.values(); } has(key: TKey): boolean { - return this.data.has(key); + return this.temp.has(key); } - set(key: TKey, value: TValue): void { - this.data.set(key, value); - this.syncData?.push([key, this.data.get(key)!]); + set(key: TKey, value: TValue): this { + this.temp.set(key, value); + return this; } get(key: TKey, defaultValue?: DefaultValueGetter): TValue { - if (this.data.has(key)) { - return this.data.get(key)!; + if (!this.temp.has(key)) { + const provider = defaultValue || this.defaultValueGetter; + + if (!provider) { + throw new Error('MetadataMap: defaultValue should be provided if defaultValueGetter not set'); + } + + const value = provider(key, this); + this.temp.set(key, observable(value as any)); } - - const provider = defaultValue || this.defaultValueGetter; - - if (!provider) { - throw new Error('MetadataMap: defaultValue should be provided if defaultValueGetter not set'); - } - - const value = provider(key, this); - untracked(() => { - this.set(key, observable(value as any)); - this.length++; - }); - return this.data.get(key)!; + return this.temp.get(key)!; } - delete(key: TKey): void { - if (this.data.has(key)) { - this.data.delete(key); - - const removeIndex = this.syncData?.findIndex(([k]) => k === key) || -1; - if (removeIndex > -1) { - this.syncData?.splice(removeIndex, 1); - } - this.length--; - } + delete(key: TKey): boolean { + return this.temp.delete(key); } clear(): void { this.data.clear(); - this.length = 0; + this.temp.clear(); this.syncData?.splice(0, this.syncData.length); } } diff --git a/webapp/packages/core-utils/src/TempMap.ts b/webapp/packages/core-utils/src/TempMap.ts index fe3e5478cd..1657712533 100644 --- a/webapp/packages/core-utils/src/TempMap.ts +++ b/webapp/packages/core-utils/src/TempMap.ts @@ -8,6 +8,8 @@ import { action } from 'mobx'; +import { cacheValue, ICachedValueObject } from './cacheValue'; + const combine = (a: IterableIterator, b: IterableIterator): IterableIterator => ( function* () { yield* a; yield* b; } )(); @@ -28,23 +30,38 @@ export class TempMap implements Map { private readonly deleted: TKey[]; private readonly temp: Map; private flushTask: NodeJS.Timeout | null; + private readonly keysTemp: ICachedValueObject; + private readonly valuesTemp: ICachedValueObject; + private readonly entriesTemp: ICachedValueObject<[TKey, TValue][]>; constructor( - private readonly target: Map + private readonly target: Map, + private readonly onSync?: () => void, ) { this.temp = new Map(); this.flushTask = null; this.deleted = []; + this.keysTemp = cacheValue(); + this.entriesTemp = cacheValue(); + this.valuesTemp = cacheValue(); } isDeleted(key: TKey): boolean { return this.deleted.includes(key); } + /** + * This function will not call clear on target map + */ clear(): void { + if (this.flushTask) { + clearTimeout(this.flushTask); + } this.deleted.splice(0, this.deleted.length); this.temp.clear(); - this.target.clear(); + this.keysTemp.invalidate(); + this.valuesTemp.invalidate(); + this.entriesTemp.invalidate(); } delete(key: TKey): boolean { @@ -88,29 +105,39 @@ export class TempMap implements Map { set(key: TKey, value: TValue): this { this.temp.set(key, value); + + const indexOfDeleted = this.deleted.indexOf(key); + if (indexOfDeleted !== -1) { + this.deleted.splice(indexOfDeleted, 1); + } + this.scheduleFlush(); return this; } entries(): IterableIterator<[TKey, TValue]> { - return Array.from(this.keys()) - .map<[TKey, TValue]>(key => [key, this.get(key)!]) + return this.entriesTemp.value(() => Array.from(this.keys()) + .map<[TKey, TValue]>(key => [key, this.get(key)!])) .values(); } keys(): IterableIterator { - return Array.from(new Set(combine(this.target.keys(), this.temp.keys()))) - .filter(key => !this.isDeleted(key)) + return this.keysTemp.value(() => Array.from(new Set(combine(this.target.keys(), this.temp.keys()))) + .filter(key => !this.isDeleted(key))) .values(); } values(): IterableIterator { - return Array.from(this.keys()) - .map(key => this.get(key)!) + return this.valuesTemp.value(() => Array.from(this.keys()) + .map(key => this.get(key)!)) .values(); } private scheduleFlush(): void { + this.keysTemp.invalidate(); + this.valuesTemp.invalidate(); + this.entriesTemp.invalidate(); + if (this.flushTask !== null) { return; } @@ -124,9 +151,13 @@ export class TempMap implements Map { for (const [key, value] of this.temp) { this.target.set(key, value); } + this.onSync?.(); this.temp.clear(); + this.keysTemp.invalidate(); + this.valuesTemp.invalidate(); + this.entriesTemp.invalidate(); this.flushTask = null; - }), 0); + }), 300); } } \ No newline at end of file diff --git a/webapp/packages/core-utils/src/cacheValue.ts b/webapp/packages/core-utils/src/cacheValue.ts new file mode 100644 index 0000000000..66d087caca --- /dev/null +++ b/webapp/packages/core-utils/src/cacheValue.ts @@ -0,0 +1,34 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 ICachedValueObject { + readonly invalid: boolean; + value(getter: () => T): T; + invalidate(): void; +} + +export function cacheValue(): ICachedValueObject { + let value: T; + let invalid = true; + + return { + value(getter: () => T) { + if (invalid) { + value = getter(); + invalid = false; + } + return value; + }, + get invalid() { + return invalid; + }, + invalidate() { + invalid = true; + }, + }; +} \ No newline at end of file diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index 0674a8c27a..446f0621be 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -10,6 +10,7 @@ export * from './underscore'; export * from './blobToData'; export * from './bytesToSize'; +export * from './cacheValue'; export * from './clsx'; export * from './copyToClipboard'; export * from './declensionOfNumber'; @@ -25,6 +26,7 @@ export * from './GlobalConstants'; export * from './ILoadableState'; export * from './errorOf'; export * from './isArraysEqual'; +export * from './isPrimitive'; export * from './isPropertiesEqual'; export * from './isValuesEqual'; export * from './md5'; diff --git a/webapp/packages/core-utils/src/isPrimitive.ts b/webapp/packages/core-utils/src/isPrimitive.ts new file mode 100644 index 0000000000..3b1b36130f --- /dev/null +++ b/webapp/packages/core-utils/src/isPrimitive.ts @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 isPrimitive(value: any): boolean { + return value === null || (typeof value !== 'object' && typeof value !== 'function'); +} \ No newline at end of file diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index 51af5a4e9c..54e3f9bf3e 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -10,7 +10,8 @@ import { untracked } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { ConnectionFolder, ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, ConnectionsManagerService, CONNECTION_FOLDER_NAME_VALIDATION, createConnectionFolderParam, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, ConnectionsManagerService, CONNECTION_FOLDER_NAME_VALIDATION, createConnectionFolderParam, createConnectionParam, getConnectionFolderIdFromNodeId, IConnectionFolderParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { getConnectionFolderId } from '@cloudbeaver/core-connections/src/NavTree/getConnectionFolderId'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -18,7 +19,7 @@ import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/cor import { LocalizationService } from '@cloudbeaver/core-localization'; import { ENodeMoveType, getNodesFromContext, INodeMoveData, NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, NAV_NODE_TYPE_FOLDER, nodeDeleteContext, ProjectsNavNodeService, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; import { getProjectNodeId, NAV_NODE_TYPE_PROJECT, ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, ResourceKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; +import { CachedMapAllKey, ResourceKeyAlias, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { createPath } from '@cloudbeaver/core-utils'; import { ActionService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, MenuService } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS, type IElementsTree } from '@cloudbeaver/plugin-navigation-tree'; @@ -36,7 +37,6 @@ interface ITargetNode { @injectable() export class ConnectionFoldersBootstrap extends Bootstrap { - constructor( private readonly localizationService: LocalizationService, private readonly userInfoResource: UserInfoResource, @@ -256,53 +256,59 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return; } - await this.connectionFolderResource.load(ConnectionFolderProjectKey(targetNode.projectId)); - - let parentFolder: ConnectionFolder | undefined; + let parentFolderParam: IConnectionFolderParam | undefined; if (targetNode.folderId) { - parentFolder = this.connectionFolderResource.fromNodeId(targetNode.folderId); - - if (!parentFolder) { - this.notificationService.logError({ title:'Can\'t create folder', message: 'Folder not found' }); - return; - } + parentFolderParam = getConnectionFolderIdFromNodeId(targetNode.folderId); } const result = await this.commonDialogService.open(FolderDialog, { value: this.localizationService.translate('ui_folder_new'), projectId: targetNode.projectId, + folder: parentFolderParam?.folderId, title: 'core_view_action_new_folder', - subTitle: parentFolder?.id, icon: '/icons/folder.svg#root', create: true, selectProject: targetNode.selectProject, - validation: async ({ folder, projectId }, setMessage) => { - const trimmed = folder.trim(); + validation: async ({ name, folder, projectId }, setMessage) => { + const trimmed = name.trim(); - if (trimmed.length === 0 || !folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) { + if (trimmed.length === 0 || !trimmed.match(CONNECTION_FOLDER_NAME_VALIDATION)) { setMessage('connections_connection_folder_validation'); return false; } - await this.connectionFolderResource.load(ConnectionFolderProjectKey(projectId)); + let parentKey: ResourceKeyAlias | IConnectionFolderParam = ConnectionFolderProjectKey(projectId); - return !this.connectionFolderResource.has(createConnectionFolderParam( - projectId, - createPath(parentFolder?.id, trimmed) - )); + if (folder) { + parentKey = createConnectionFolderParam( + projectId, + folder + ); + } + + try { + await this.connectionFolderResource.load(parentKey); + + return !this.connectionFolderResource.has(createConnectionFolderParam( + projectId, + createPath(folder, trimmed) + )); + } catch (exception: any) { + setMessage('connections_connection_folder_validation'); + return false; + } }, }); if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { try { - await this.connectionFolderResource.create(createConnectionFolderParam( - result.projectId, - result.folder - ), parentFolder?.id); + await this.connectionFolderResource.create(result.projectId, result.name, result.folder); this.navTreeResource.markOutdated( - targetNode.folderId !== undefined - ? targetNode.folderId + result.folder + ? getConnectionFolderId(createConnectionFolderParam(result.projectId, result.folder)) : getProjectNodeId(result.projectId) ); } catch (exception: any) { diff --git a/webapp/packages/plugin-devtools/src/DevToolsService.ts b/webapp/packages/plugin-devtools/src/DevToolsService.ts index 832dfb55bd..ca7b94ff97 100644 --- a/webapp/packages/plugin-devtools/src/DevToolsService.ts +++ b/webapp/packages/plugin-devtools/src/DevToolsService.ts @@ -9,10 +9,12 @@ import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; interface IDevToolsSettings { enabled: boolean; + distributed: boolean; } const DEVTOOLS = 'devtools'; @@ -23,9 +25,14 @@ export class DevToolsService { return this.settings.enabled; } + get isDistributed(): boolean { + return this.settings.distributed; + } + private readonly settings: IDevToolsSettings; constructor( + private readonly serverConfigResource: ServerConfigResource, private readonly autoSaveService: LocalStorageSaveService, ) { this.settings = getDefaultDevToolsSettings(); @@ -34,15 +41,31 @@ export class DevToolsService { settings: observable, }); this.autoSaveService.withAutoSave(DEVTOOLS, this.settings, getDefaultDevToolsSettings); + this.serverConfigResource.onDataUpdate.addHandler(this.syncDistributedMode.bind(this)); } switch() { this.settings.enabled = !this.settings.enabled; + this.syncDistributedMode(); + } + + setDistributedMode(distributed: boolean) { + this.settings.distributed = distributed; + this.syncDistributedMode(); + } + + private syncDistributedMode() { + if (this.isEnabled) { + if (this.serverConfigResource.data) { + this.serverConfigResource.data.distributed = this.isDistributed; + } + } } } function getDefaultDevToolsSettings(): IDevToolsSettings { return { enabled: false, + distributed: false, }; } \ No newline at end of file diff --git a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts index b4ba9f9ea5..0767c0f444 100644 --- a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts @@ -15,6 +15,7 @@ import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu'; import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-user-profile'; import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS'; +import { ACTION_DEVTOOLS_MODE_DISTRIBUTED } from './actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED'; import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH'; import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem'; import { DevToolsService } from './DevToolsService'; @@ -109,12 +110,22 @@ export class PluginBootstrap extends Bootstrap { return [ new SearchResourceMenuItem(), + ACTION_DEVTOOLS_MODE_DISTRIBUTED, MENU_PLUGINS, ...items, ]; }, }); + this.actionService.addHandler({ + id: 'devtools-mode-distributed', + isActionApplicable: (context, action) => action === ACTION_DEVTOOLS_MODE_DISTRIBUTED, + isChecked: () => this.devToolsService.isDistributed, + handler: () => { + this.devToolsService.setDistributedMode(!this.devToolsService.isDistributed); + }, + }); + this.menuService.addCreator({ isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_PLUGINS, getItems: (context, items) => [ diff --git a/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED.ts b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED.ts new file mode 100644 index 0000000000..ddd4317c8c --- /dev/null +++ b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DEVTOOLS_MODE_DISTRIBUTED = createAction('devtools-mode-distributed', { + type: 'checkbox', + label: 'Distributed mode', + tooltip: 'Enable distributed mode', +}); \ No newline at end of file diff --git a/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsService.ts b/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsService.ts index 5008004d6c..fcf37984aa 100644 --- a/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsService.ts @@ -131,7 +131,7 @@ export class NavigationTabsService extends View { this.onTabClose = new SyncExecutor(); this.onInit = new SyncExecutor(); this.onStateUpdate = new SyncExecutor(); - this.metadata = new MetadataMap(() => ({ restored: false })); + this.metadata = new MetadataMap(() => ({ restored: false })); this.navigationTabContext = (): ITabNavigationContext => new TabNavigationContext(this, this.userInfoResource); this.registerAction(ACTION_OPEN_IN_TAB); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx index c013bc6b9b..c6db08d1d0 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx @@ -14,7 +14,6 @@ import { EventTreeNodeClickFlag, EventTreeNodeExpandFlag, EventTreeNodeSelectFla import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; import { type NavNode, ROOT_NODE_PATH, NavTreeResource, NavNodeInfoResource, EObjectFeature } from '@cloudbeaver/core-navigation-tree'; -import { resourceKeyList } from '@cloudbeaver/core-sdk'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; import { useNavTreeDropBox } from '../useNavTreeDropBox'; @@ -31,6 +30,7 @@ import { elementsTreeLimitFilter } from './NavTreeLimitFilter/elementsTreeLimitF import { elementsTreeLimitRenderer } from './NavTreeLimitFilter/elementsTreeLimitRenderer'; import { useDropOutside } from './useDropOutside'; import { IElementsTreeOptions, useElementsTree } from './useElementsTree'; +import { useElementsTreeFolderExplorer } from './useElementsTreeFolderExplorer'; const styles = css` box { @@ -65,7 +65,7 @@ const styles = css` } FolderExplorerPath { - padding: 0 12px 8px 12px; + padding: 0 4px 8px 4px; } drop-outside { @@ -133,61 +133,13 @@ export const ElementsTree = observer(function ElementsTree({ onSelect, onFilter, }) { - const folderExplorer = useFolderExplorer(baseRoot, { - saveState: settings?.saveExpanded, - }); const navTreeResource = useService(NavTreeResource); const navNodeInfoResource = useService(NavNodeInfoResource); - const ref = useObjectRef({ settings, getChildren, loadChildren }); const treeRootRef = useRef(null); + const folderExplorer = useElementsTreeFolderExplorer(baseRoot, settings); const root = folderExplorer.state.folder; - function exitFolders(path: string[]) { - path = path.filter(nodeId => ref.getChildren(nodeId) !== undefined); - if (path.length > 0) { - folderExplorer.open(path.slice(0, path.length - 1), path[path.length - 1]); - } - } - - const autoOpenFolders = useCallback(async function autoOpenFolders(nodeId: string, path: string[]) { - path = [...path]; - - if (!ref.settings?.foldersTree && !folderExplorer.options.expandFoldersWithSingleElement) { - return; - } - - while (folderExplorer.options.expandFoldersWithSingleElement) { - const children = ref.getChildren(nodeId); - - if (children?.length === 1) { - const nextNodeId = children[0]; - const loaded = await ref.loadChildren(nextNodeId, false); - - if (!loaded) { - break; - } - - path.push(nodeId); - nodeId = nextNodeId; - } else { - break; - } - } - - folderExplorer.open(path, nodeId); - - }, [folderExplorer]); - - useResource(ElementsTree, navTreeResource, resourceKeyList(folderExplorer.state.fullPath), { - onError: () => { - exitFolders(folderExplorer.state.fullPath); - }, - onData: () => { - autoOpenFolders(folderExplorer.state.folder, folderExplorer.state.path); - }, - }); - const limitFilter = useMemo(() => elementsTreeLimitFilter( navTreeResource, limit @@ -221,6 +173,8 @@ export const ElementsTree = observer(function ElementsTree({ customSelect, onExpand, onSelect, + onOpen, + onClick, }); const context = useMemo( @@ -230,24 +184,8 @@ export const ElementsTree = observer(function ElementsTree({ selectionTree, control, getTreeRoot: () => treeRootRef.current, - onOpen: async (node, path, leaf) => { - const folder = !leaf && tree.settings?.foldersTree || false; - - await onOpen?.(node, folder); - - if (!leaf && tree.settings?.foldersTree) { - const nodeId = node.id; - const loaded = await ref.loadChildren(nodeId, true); - - if (loaded) { - await autoOpenFolders(nodeId, path); - tree.setFilter(''); - } - } - }, - onClick, }), - [control, folderExplorer, selectionTree, onOpen, onClick, folderExplorer] + [tree, folderExplorer, selectionTree, control] ); const getName = useCallback( @@ -281,22 +219,6 @@ export const ElementsTree = observer(function ElementsTree({ tree.resetSelection(); } - const foldersTree = settings?.foldersTree; // mobx subscription - const filter = settings?.filter; - - useEffect(() => { - if (!foldersTree && folderExplorer.state.folder !== baseRoot) { - folderExplorer.open([], baseRoot); - } - if (!filter && tree.filtering) { - tree.setFilter(''); - } - }); - - const children = tree.getNodeChildren(root); - const hasChildren = children.length > 0; - const loaderAvailable = !foldersTree || context.folderExplorer.root === root; - return styled(useStyles(TREE_NODE_STYLES, styles, style))( <> @@ -319,11 +241,9 @@ export const ElementsTree = observer(function ElementsTree({ (function ElementsTree({ root /> - {loaderAvailable && } diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeContentLoader.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeContentLoader.tsx index 2cfd6a7eb2..bbb14d571b 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeContentLoader.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeContentLoader.tsx @@ -7,58 +7,36 @@ */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; +import styled from 'reshadow'; -import { Loader, Translate, TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; +import { getComputed, Loader, Translate, TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; import type { ILoadableState } from '@cloudbeaver/core-utils'; import type { IElementsTreeContext } from './ElementsTreeContext'; -const styles = css` - center { - display: flex; - height: 100%; - width: 100%; - min-width: 240px; - } -`; - interface Props { - root: string; context: IElementsTreeContext; childrenState: ILoadableState; emptyPlaceholder?: React.FC; - hasChildren?: boolean; } export const ElementsTreeContentLoader = observer>(function ElementsTreeContentLoader({ - root, context, childrenState, emptyPlaceholder: Placeholder, - hasChildren, children, }) { - let loading = childrenState.isLoading() || context.tree.loading; + const hasChildren = getComputed(() => context.tree.getNodeChildren(context.tree.root).length > 0); - if (context.tree.settings?.foldersTree && context.folderExplorer.root !== root) { - loading = false; - } - - if (context.tree.settings?.saveExpanded && context.tree.loading) { - return styled(styles)( -
- -
- ); - } + const loading = getComputed(() => ( + childrenState.isLoading() + || context.tree.loading + ) && !context.tree.isLoaded()); if (!hasChildren) { if (loading) { - return styled(styles)( -
- -
+ return ( + ); } else if (context.tree.filtering) { return styled(TREE_NODE_STYLES)( @@ -72,8 +50,8 @@ export const ElementsTreeContentLoader = observer } return ( - <> + {children} - + ); }); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeContext.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeContext.ts index 2bb948330f..1ba49ae8b1 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeContext.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeContext.ts @@ -9,7 +9,6 @@ import { createContext } from 'react'; import type { IFolderExplorerContext } from '@cloudbeaver/core-blocks'; -import type { NavNode } from '@cloudbeaver/core-navigation-tree'; import type { NavTreeControlComponent } from './NavigationNodeComponent'; import type { IElementsTree } from './useElementsTree'; @@ -20,8 +19,6 @@ export interface IElementsTreeContext { selectionTree: boolean; control?: NavTreeControlComponent; getTreeRoot: () => HTMLDivElement | null; - onOpen?: (node: NavNode, path: string[], leaf: boolean) => Promise | void; - onClick?: (node: NavNode, path: string[], leaf: boolean) => Promise | void; } export const ElementsTreeContext = createContext(null); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx index c98e9375dd..110a69d77f 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx @@ -11,7 +11,6 @@ import React, { useState } from 'react'; import styled, { css, use } from 'reshadow'; import { ACTION_ICON_BUTTON_STYLES, IconButton, PlaceholderElement, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import { NavTreeResource } from '@cloudbeaver/core-navigation-tree'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; import { useCaptureViewContext } from '@cloudbeaver/core-view'; @@ -80,14 +79,13 @@ export const ElementsTreeTools = observer>(functi const translate = useTranslate(); const [opened, setOpen] = useState(false); const styles = useStyles(ACTION_ICON_BUTTON_STYLES, toolsStyles, style); - const rootNode = useResource(ElementsTreeTools, NavTreeResource, root); useCaptureViewContext(context => { context?.set(DATA_CONTEXT_NAV_TREE_ROOT, tree.baseRoot); context?.set(DATA_CONTEXT_ELEMENTS_TREE, tree); }); - const loading = rootNode.isLoading(); + const loading = tree.isLoading(); return styled(styles)( diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx index c46f393f40..21106f4b70 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx @@ -45,7 +45,6 @@ export const NavigationNodeNested = observer(forwardRef(f !!root && treeContext?.folderExplorer.state.folder !== treeContext?.folderExplorer.root )); - if (!rootFolder) { children = getComputed( () => treeContext?.tree.getNodeChildren(nodeId) || [], diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts index ca630d71ff..df65d66f70 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts @@ -73,8 +73,8 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod () => contextRef.context?.tree.isNodeIndeterminateSelected(node.id) || false ); - const handleClick = async (leaf: boolean) => await contextRef.context?.onClick?.(node, path, leaf); - const handleOpen = async (leaf: boolean) => await contextRef.context?.onOpen?.(node, path, leaf); + const handleClick = async (leaf: boolean) => await contextRef.context?.tree.click(node, path, leaf); + const handleOpen = async (leaf: boolean) => await contextRef.context?.tree.open(node, path, leaf); const handleExpand = async () => await contextRef.context?.tree.expand(node, !expanded); const handleSelect = async ( multiple = false, diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts index 09d5b9ac58..bbf50452ef 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -9,7 +9,7 @@ import { action, computed, observable, runInAction } from 'mobx'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { IFolderExplorerContext, useExecutor, useResource, useObjectRef, useObservableRef, useUserData } from '@cloudbeaver/core-blocks'; +import { IFolderExplorerContext, useExecutor, useResource, useObjectRef, useObservableRef, useUserData, getComputed } from '@cloudbeaver/core-blocks'; import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -78,6 +78,8 @@ export interface IElementsTreeOptions { onFilter?: (value: string) => Promise | void; onSelect?: (node: NavNode, state: boolean) => Promise | void; onExpand?: (node: NavNode, state: boolean) => Promise | void; + onOpen?: (node: NavNode, folder: boolean) => Promise | void; + onClick?: (node: NavNode) => Promise | void; isGroup?: (node: NavNode) => boolean; customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise | void; beforeSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise | void; @@ -117,10 +119,13 @@ export interface IElementsTree extends ILoadableState { select: (node: NavNode, multiple: boolean, nested: boolean) => Promise; resetSelection(): Promise; setDnDData: (data: IDNDData, dragging: boolean) => void; + open: (node: NavNode, path: string[], leaf: boolean) => Promise; + click: (node: NavNode, path: string[], leaf: boolean) => Promise; expand: (node: NavNode, state: boolean) => Promise; show: (nodeId: string, parents: string[]) => Promise; refresh: (nodeId: string) => Promise; collapse: () => void; + loadPath: (path: string[], lastNode?: string) => Promise; } export function useElementsTree(options: IOptions): IElementsTree { @@ -145,52 +150,96 @@ export function useElementsTree(options: IOptions): IElementsTree { const functionsRef = useObjectRef({ async loadTree(nodeId: string) { - await connectionInfoResource.load(ConnectionInfoActiveProjectKey); - const preloaded = await navTreeResource.preloadNodeParents(options.folderExplorer.state.fullPath); + elementsTree.loading = true; + try { + await projectInfoResource.load(); + await connectionInfoResource.load(ConnectionInfoActiveProjectKey); + const preloadedRoot = await elementsTree.loadPath(options.folderExplorer.state.fullPath); - if (!preloaded) { - return; - } + if (preloadedRoot !== options.folderExplorer.state.folder) { + if (preloadedRoot === undefined) { + options.folderExplorer.open([], options.baseRoot); + } else { + this.exitNodeFolder(preloadedRoot); + } + return; + } - let children = [nodeId]; + let children = [nodeId]; - while (children.length > 0) { - const nextChildren: string[] = []; + while (children.length > 0) { + const nextChildren: string[] = []; - await Promise.all(children.map(async child => { - await navTreeResource.waitLoad(); - await navNodeInfoResource.waitLoad(); + await Promise.all(children.map(async child => { + await projectInfoResource.waitLoad(); + await connectionInfoResource.waitLoad(); + await navTreeResource.waitLoad(); + await navNodeInfoResource.waitLoad(); - const expanded = elementsTree.isNodeExpanded(child, true); - if (!expanded && child !== options.root) { - if (navNodeInfoResource.isOutdated(child)) { + const expanded = elementsTree.isNodeExpanded(child, true); + if (!expanded && child !== options.root) { + if (navNodeInfoResource.isOutdated(child)) { + const node = navNodeInfoResource.get(child); + + if (node?.parentId !== undefined && !navTreeResource.isOutdated(node.parentId)) { + await navNodeInfoResource.load(child); + } + } + return; + } + + const loaded = await options.loadChildren(child, false); + + if (!loaded) { const node = navNodeInfoResource.get(child); - if (node?.parentId !== undefined && !navTreeResource.isOutdated(node.parentId)) { - await navNodeInfoResource.load(child); + if (node) { + await elementsTree.expand(node, false); + } + return; + } + + if ( + elementsTree.settings?.foldersTree + && options.folderExplorer.options.expandFoldersWithSingleElement + && child === options.root + && elementsTree.getNodeChildren(child).length === 1 + ) { + const nextNode = elementsTree.getNodeChildren(child)[0]; + + if (elementsTree.isNodeExpandable(nextNode) || elementsTree.isNodeExpanded(nextNode)) { + options.folderExplorer.open(navNodeInfoResource.getParents(nextNode), nextNode); } } - return; - } + nextChildren.push(...(navTreeResource.get(child) || [])); + })); - const loaded = await options.loadChildren(child, false); - - if (!loaded) { - const node = navNodeInfoResource.get(child); - - if (node) { - await elementsTree.expand(node, false); - } - return; - } - - nextChildren.push(...(navTreeResource.get(child) || [])); - })); - - children = nextChildren; + children = nextChildren; + } + } finally { + elementsTree.loading = false; } }, + exitNodeFolder(nodeId: string) { + runInAction(() => { + const folderExplorer = options.folderExplorer; + + if (folderExplorer.state.fullPath.length === 1 || nodeId === options.baseRoot) { + return; + } + + const pathIndex = folderExplorer.state.fullPath.indexOf(nodeId); + + if (pathIndex >= 0) { + folderExplorer.open( + folderExplorer.state.fullPath.slice(0, pathIndex - 1), + folderExplorer.state.fullPath[pathIndex - 1] + ); + } + }); + }, + getNestedChildren(nodeId: string): string[] { const nestedChildren: string[] = []; const prevChildren = elementsTree.getNodeChildren(nodeId); @@ -292,12 +341,7 @@ export function useElementsTree(options: IOptions): IElementsTree { state.sync(data.nodeState); - elementsTree.loading = true; - try { - await functionsRef.loadTree(options.root); - } finally { - elementsTree.loading = false; - } + await functionsRef.loadTree(options.root); }, data => ( typeof data === 'object' @@ -314,14 +358,14 @@ export function useElementsTree(options: IOptions): IElementsTree { get filter(): string { return this.userData.filter; }, - get filtering() { + get filtering(): boolean { return this.filter !== ''; }, - isLoading() { + isLoading(): boolean { return this.loading; }, - isLoaded() { - return true; + isLoaded(): boolean { + return navNodeInfoResource.isLoaded(this.root); }, getNodeState(nodeId: string) { return this.state.get(nodeId); @@ -407,6 +451,7 @@ export function useElementsTree(options: IOptions): IElementsTree { }, getNodeChildren(nodeId: string): string[] { const node = navNodeInfoResource.get(nodeId); + const children = options.getChildren(nodeId) || []; if (!node) { return []; // Maybe filter should accept nodeId, so we be able to apply filters to empty node @@ -415,7 +460,7 @@ export function useElementsTree(options: IOptions): IElementsTree { return (options.filters || []) .reduce( (children, filter) => filter(elementsTree, elementsTree.filter, node, children, this.state), - options.getChildren(node.id) || [] + children ); }, async setFilter(value: string) { @@ -443,16 +488,14 @@ export function useElementsTree(options: IOptions): IElementsTree { } }, async show(nodeId: string, path: string[]): Promise { - const preloaded = await navTreeResource.preloadNodeParents(path, nodeId); + const preloaded = await this.loadPath(path, nodeId); - if (!preloaded) { + if (preloaded !== nodeId) { notificationService.logError({ title: 'app_navigationTree_node_not_found', message: nodeId, }); - } - - if (preloaded) { + } else { runInAction(() => { for (const parent of path) { const state = this.getNodeState(parent); @@ -470,6 +513,24 @@ export function useElementsTree(options: IOptions): IElementsTree { } } }, + async click(node: NavNode, path: string[], leaf: boolean) { + await options.onClick?.(node); + }, + async open(node: NavNode, path: string[], leaf: boolean) { + const expandableOrExpanded = this.isNodeExpandable(node.id) || this.isNodeExpanded(node.id); + if (!leaf && this.settings?.foldersTree && expandableOrExpanded) { + const nodeId = node.id; + + const loaded = await options.loadChildren(node.id, true); + if (loaded) { + this.setFilter(''); + options.folderExplorer.open(path, nodeId); + } + } + + const folder = !leaf && this.settings?.foldersTree || false; + await options.onOpen?.(node, folder); + }, async expand(node: NavNode, state: boolean) { if (!this.isNodeExpandable(node.id)) { return; @@ -538,6 +599,26 @@ export function useElementsTree(options: IOptions): IElementsTree { } await functionsRef.resetSelection(); }, + async loadPath(path: string[], lastNode?: string): Promise { + let lastLoadedNode: string | undefined; + for (const nodeId of path) { + const loaded = await options.loadChildren(nodeId, false); + + if (!loaded) { + return lastLoadedNode; + } + lastLoadedNode = nodeId; + } + + if ( + lastNode !== undefined + && lastLoadedNode !== undefined + && options.getChildren(lastLoadedNode)?.includes(lastNode)) { + return lastNode; + } + + return lastLoadedNode; + }, setDnDData(data: IDNDData, dragging: boolean) { if (dragging) { if (!this.activeDnDData.includes(data)) { @@ -570,25 +651,6 @@ export function useElementsTree(options: IOptions): IElementsTree { userData, }, ['isLoading', 'isLoaded']); - function exitNodeFolder(nodeId: string) { - runInAction(() => { - const folderExplorer = options.folderExplorer; - - if (folderExplorer.state.fullPath.length === 1 || nodeId === options.baseRoot) { - return; - } - - const pathIndex = folderExplorer.state.fullPath.indexOf(nodeId); - - if (pathIndex >= 0) { - folderExplorer.open( - folderExplorer.state.fullPath.slice(0, pathIndex - 1), - folderExplorer.state.fullPath[pathIndex - 1] - ); - } - }); - } - useEffect(() => { functionsRef.loadTree(options.root); }, [options.root]); @@ -597,6 +659,10 @@ export function useElementsTree(options: IOptions): IElementsTree { functionsRef.loadTree(options.root); }, 100), []); + useResource(useElementsTree, navTreeResource, options.baseRoot, { + onData: () => loadTreeThreshold(), + }); + useResource(useElementsTree, ProjectInfoResource, CachedMapAllKey, { onData: () => { loadTreeThreshold(); @@ -631,7 +697,7 @@ export function useElementsTree(options: IOptions): IElementsTree { const children = navTreeResource.get(key); if (!children) { - exitNodeFolder(key); + functionsRef.exitNodeFolder(key); } }); }], @@ -657,11 +723,21 @@ export function useElementsTree(options: IOptions): IElementsTree { if (node) { await elementsTree.expand(node, false); - exitNodeFolder(key); + functionsRef.exitNodeFolder(key); } }); }], }); + + // sync settings + const filterDisabled = getComputed(() => !options.settings?.filter && elementsTree.filtering); + + useEffect(() => { + if (filterDisabled) { + elementsTree.setFilter(''); + } + }); + return elementsTree; } diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTreeFolderExplorer.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTreeFolderExplorer.ts new file mode 100644 index 0000000000..189b4367c5 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTreeFolderExplorer.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { useEffect } from 'react'; + +import { getComputed, useFolderExplorer } from '@cloudbeaver/core-blocks'; + +import type { IElementsTreeSettings } from './useElementsTree'; + +export function useElementsTreeFolderExplorer( + baseRoot: string, + settings: IElementsTreeSettings | undefined +) { + const folderExplorer = useFolderExplorer(baseRoot, { + saveState: settings?.saveExpanded, + }); + + const foldersTreeDisabled = getComputed(() => !settings?.foldersTree && folderExplorer.state.folder !== baseRoot); + + useEffect(() => { + if (foldersTreeDisabled) { + folderExplorer.open([], baseRoot); + } + }); + + return folderExplorer; +} \ No newline at end of file diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx index 5d63e3a77f..d4f8899613 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx @@ -84,7 +84,6 @@ export const NavigationTree = observer(function NavigationTree() { const navNodeViewService = useService(NavNodeViewService); const root = ROOT_NODE_PATH; - const { authenticated } = useService(AppAuthService); const { handleOpen, handleSelect, handleSelectReset } = useNavigationTree(); const connectionGroupFilter = useMemo(() => navigationTreeConnectionGroupFilter( @@ -114,10 +113,6 @@ export const NavigationTree = observer(function NavigationTree() { const settingsElements = useMemo(() => ([ProjectsSettingsPlaceholderElement]), []); - if (!authenticated) { - return null; - } - return styled(navigationTreeStyles)( { private readonly connectionsManagerService: ConnectionsManagerService, private readonly connectionInfoResource: ConnectionInfoResource, private readonly navNodeExtensionsService: NavNodeExtensionsService, + private readonly navNodeInfoResource: NavNodeInfoResource, private readonly navTreeResource: NavTreeResource ) { super(); - this.treeState = new MetadataMap(() => ({ + this.treeState = new MetadataMap(() => ({ showInFilter: false, expanded: false, selected: false, @@ -99,6 +100,12 @@ export class NavigationTreeService extends View { this.navTreeResource.markOutdated(id); } + const parents = this.navNodeInfoResource.getParents(id); + + if (parents.length > 0 && !this.navNodeInfoResource.has(id)) { + return false; + } + await this.navTreeResource.load(id); return true; } catch (exception: any) { diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx index 0f888650ab..d25c8c44eb 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -92,6 +92,10 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer) { + elementsTreeContext?.tree.open(node, navNodeInfoResource.getParents(node.id), false); + } + if (elementsTreeContext?.tree.settings?.projects === false && !isDragging) { return null; } @@ -100,6 +104,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPage/ObjectPage.ts b/webapp/packages/plugin-object-viewer/src/ObjectPage/ObjectPage.ts index 218823a361..6480755f15 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPage/ObjectPage.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPage/ObjectPage.ts @@ -8,7 +8,7 @@ import { observable, makeObservable } from 'mobx'; -import type { DynamicStyle } from '@cloudbeaver/core-theming'; +import type { ComponentStyle } from '@cloudbeaver/core-theming'; import type { ITab } from '@cloudbeaver/plugin-navigation-tabs'; import type { IObjectViewerTabState } from '../IObjectViewerTabState'; @@ -17,7 +17,7 @@ export interface ObjectPageTabProps { tab: ITab; page: ObjectPage; onSelect: () => void; - style: DynamicStyle | DynamicStyle[]; + style: ComponentStyle; } export type ObjectPageTabComponent = React.FunctionComponent>; diff --git a/webapp/packages/plugin-projects/src/FolderDialog.tsx b/webapp/packages/plugin-projects/src/FolderDialog.tsx index b6e5a66427..035f05727f 100644 --- a/webapp/packages/plugin-projects/src/FolderDialog.tsx +++ b/webapp/packages/plugin-projects/src/FolderDialog.tsx @@ -11,9 +11,10 @@ import { observer } from 'mobx-react-lite'; import { useEffect } from 'react'; import styled, { css } from 'reshadow'; -import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, Translate, useFocus, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, Translate, useFocus, useObservableRef, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs'; -import { throttleAsync } from '@cloudbeaver/core-utils'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { createPath, throttleAsync } from '@cloudbeaver/core-utils'; import { ProjectSelect } from '@cloudbeaver/plugin-projects'; const style = css` @@ -29,26 +30,30 @@ const style = css` interface IFolderDialogState { value: string; projectId: string; + folder?: string; message: string | undefined; valid: boolean; payload: FolderDialogPayload; + validationInProgress: boolean; validate: () => Promise; setMessage: (message: string) => void; setProjectId: (projectId: string) => void; } export interface IFolderDialogResult { - folder: string; + folder?: string; + name: string; projectId: string; } export interface FolderDialogPayload { value: string; projectId: string; + folder?: string; + selectProject: boolean; objectName?: string; icon?: string; - subTitle?: string; bigIcon?: boolean; viewBox?: string; confirmActionText?: string; @@ -68,7 +73,7 @@ export const FolderDialog: DialogComponent(() => ({ value, projectId, + folder, message: undefined, valid: true, + validationInProgress: false, validate: throttleAsync(async () => { + const { folder, value, projectId } = state; state.message = undefined; - state.valid = (await state.payload.validation?.( - { folder:state.value, projectId: state.projectId }, - state.setMessage.bind(state) - )) ?? true; + state.validationInProgress = true; + let valid: boolean | undefined; + try { + valid = await state.payload.validation?.( + { folder, name: value, projectId }, + (message: string) => { + if (state.folder === folder && state.value === value && state.projectId === projectId) { + state.setMessage(message); + } + } + ); + } catch {} + + if (state.folder === folder && state.value === value && state.projectId === projectId) { + state.valid = valid ?? true; + state.validationInProgress = false; + } }, 300), setMessage(message) { this.message = message; }, setProjectId(projectId) { this.projectId = projectId; + this.folder = undefined; }, }), { value: observable.ref, projectId: observable.ref, + validationInProgress: observable.ref, + folder: observable.ref, valid: observable.ref, message: observable.ref, }, { payload, }); + const projectInfoLoader = useResource(FolderDialog, ProjectInfoResource, state.projectId); + async function resolveHandler() { await state.validate(); if (state.valid) { - resolveDialog({ folder: state.value, projectId: state.projectId }); + resolveDialog({ folder: state.folder, name: state.value, projectId: state.projectId }); } } @@ -129,6 +155,7 @@ export const FolderDialog: DialogComponent @@ -154,6 +181,7 @@ export const FolderDialog: DialogComponent state.validate()} > {translate('ui_name') + ':'} diff --git a/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts b/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts index 6bb66ff7ca..45eefa7029 100644 --- a/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts +++ b/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts @@ -11,13 +11,13 @@ import { CONNECTION_FOLDER_NAME_VALIDATION } from '@cloudbeaver/core-connections import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { DialogueStateResult, CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { executorHandlerFilter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { NavTreeResource, NavNodeManagerService, NavNodeInfoResource, type INodeMoveData, navNodeMoveContext, getNodesFromContext, ENodeMoveType, ProjectsNavNodeService } from '@cloudbeaver/core-navigation-tree'; import { ProjectInfo, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; import { getRmResourceKey, getRmResourcePath, NAV_NODE_TYPE_RM_PROJECT, NAV_NODE_TYPE_RM_RESOURCE, ResourceManagerResource, RESOURCES_NODE_PATH } from '@cloudbeaver/core-resource-manager'; import { CachedMapAllKey, CachedTreeChildrenKey, getCachedMapResourceLoaderState, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; -import { createPath, getPathParent, isDefined } from '@cloudbeaver/core-utils'; +import { createPath, getPathParent } from '@cloudbeaver/core-utils'; import { ActionService, MenuService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, DATA_CONTEXT_LOADABLE_STATE } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeaver/plugin-navigation-tree'; import { FolderDialog } from '@cloudbeaver/plugin-projects'; @@ -164,18 +164,13 @@ export class ResourceFoldersBootstrap extends Bootstrap { let path: string | undefined; - if (!targetNode.folderId) { - const project = this.projectInfoResource.get(targetNode.projectId); - targetNode.folderId = this.getResourceTypeFolder(project!, resourceTypeId); - } - if (targetNode.folderId) { const resourceKey = getResourceKeyFromNodeId(targetNode.folderId); if (resourceKey !== undefined) { const key = getRmResourceKey(resourceKey); if (key.path) { - path = resourceKey; + path = key.path; } } } @@ -183,32 +178,39 @@ export class ResourceFoldersBootstrap extends Bootstrap { const result = await this.commonDialogService.open(FolderDialog, { value: this.localizationService.translate('ui_folder_new'), projectId: targetNode.projectId, + folder: path, title: 'core_view_action_new_folder', - subTitle: path, icon: '/icons/folder.svg#root', create: true, selectProject: targetNode.selectProject, - validation: async ({ folder, projectId }, setMessage) => { - const trimmed = folder.trim(); + validation: async ({ name, folder, projectId }, setMessage) => { + const trimmed = name.trim(); - if (trimmed.length === 0 || !folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) { + if (trimmed.length === 0 || !name.match(CONNECTION_FOLDER_NAME_VALIDATION)) { setMessage('connections_connection_folder_validation'); return false; } - const key = path !== undefined ? path : getRmResourcePath(projectId); + const root = this.getResourceTypeFolder(projectId, resourceTypeId); + const key = getRmResourcePath(projectId, folder ?? root); - await this.resourceManagerResource.load(CachedTreeChildrenKey(key)); + try { + await this.resourceManagerResource.load(CachedTreeChildrenKey(key)); - return !this.resourceManagerResource.has(createPath(key, trimmed)); + return !this.resourceManagerResource.has(createPath(key, trimmed)); + } catch (exception: any) { + setMessage('connections_connection_folder_validation'); + return false; + } }, }); if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { try { - const key = path !== undefined ? path : getRmResourcePath(result.projectId); + const root = this.getResourceTypeFolder(result.projectId, resourceTypeId); + const key = getRmResourcePath(result.projectId, result.folder ?? root); await this.resourceManagerResource.create( - createPath(key, result.folder), + createPath(key, result.name), true ); @@ -276,18 +278,22 @@ export class ResourceFoldersBootstrap extends Bootstrap { }; } - private getResourceTypeFolder(project: ProjectInfo, resourceTypeId: string | undefined): string | undefined { + private getResourceTypeFolder(projectId: string, resourceTypeId: string | undefined): string | undefined { if (!resourceTypeId) { return undefined; } + const project = this.projectInfoResource.get(projectId); + + if (!project) { + return undefined; + } const resourceFolder = this.resourceManagerService.getRootFolder(project, resourceTypeId); - return createPath(RESOURCES_NODE_PATH, project.id, resourceFolder); + return resourceFolder; } private syncNavTree() { this.navNodeManagerService.onMove.addHandler(this.moveResourceToFolder.bind(this)); - let syncOutdate = true; // this.navNodeInfoResource.onItemUpdate.addHandler(executorHandlerFilter( // () => syncOutdate, @@ -310,31 +316,29 @@ export class ResourceFoldersBootstrap extends Bootstrap { // } // )); - this.navNodeInfoResource.onItemDelete.addHandler(executorHandlerFilter( - () => true, - key => { - const resources = ResourceKeyUtils - .mapArray(key, getResourceKeyFromNodeId) - .filter(isDefined); + // this.navNodeInfoResource.onItemDelete.addHandler(executorHandlerFilter( + // () => true, + // key => { + // const resources = ResourceKeyUtils + // .mapArray(key, getResourceKeyFromNodeId) + // .filter(isDefined); - this.resourceManagerResource.delete(resourceKeyList(resources)); - } - )); + // this.resourceManagerResource.delete(resourceKeyList(resources)); + // } + // )); - this.resourceManagerResource.onItemUpdate.addHandler(executorHandlerFilter( - () => syncOutdate, - key => { - syncOutdate = false; - try { - const updated = resourceKeyList([...new Set(ResourceKeyUtils.mapArray(key, getResourceNodeId) - .map(getPathParent))]); - if (!this.navTreeResource.isOutdated(updated)) { - this.navTreeResource.markTreeOutdated(updated); - } - } finally { - syncOutdate = true; - } + this.resourceManagerResource.onItemUpdate.addHandler(key => { + const updated = resourceKeyList([...new Set(ResourceKeyUtils.mapArray(key, getResourceNodeId) + .map(getPathParent))]); + if (!this.navTreeResource.isOutdated(updated)) { + this.navTreeResource.markTreeOutdated(updated); } - )); + }); + + this.resourceManagerResource.onItemDelete.addHandler(key => { + const updated = resourceKeyList([...new Set(ResourceKeyUtils.mapArray(key, getResourceNodeId) + .map(getPathParent))]); + this.navTreeResource.deleteInNode(updated, ResourceKeyUtils.toArray(key).map(value => [value])); + }); } } \ No newline at end of file diff --git a/webapp/readme.md b/webapp/readme.md index 6daf2f8948..1aff00abce 100644 --- a/webapp/readme.md +++ b/webapp/readme.md @@ -1,25 +1,26 @@ -### Steps to run +# Steps to run +## Install Dependencies -1. install ```yarn``` globaly -2. execute ```yarn``` in this folder -3. execute ```lerna bootstrap``` -3. execute ```lerna run build --stream --scope=@cloudbeaver/product-default``` -4. open ```packages/product-default/lib/index.html``` or run ```npx serve packages/product-default/lib``` +```sh +yarn +yarn lerna bootstrap +``` ## Build +```sh +yarn lerna run build --stream --scope=@cloudbeaver/product-default ``` -lerna run build --stream --scope=@cloudbeaver/product-default -``` +open ```packages/product-default/lib/index.html``` or run ```npx serve packages/product-default/lib``` ## Dev -``` -lerna run dev --stream --scope=@cloudbeaver/product-default +```sh +yarn lerna run dev --stream --scope=@cloudbeaver/product-default ``` ## Build GQL SDK ```sh -lerna run gql:gen --stream +yarn lerna run gql:gen --stream ```