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
This commit is contained in:
Alexey
2023-04-04 12:52:35 +03:00
committed by GitHub
parent e04e2d4c37
commit e659a64cbd
51 changed files with 960 additions and 636 deletions
@@ -50,7 +50,7 @@ export const Main = observer(function Main() {
mode={leftBarDisabled ? 'minimize' : splitMainState.mode}
disable={leftBarDisabled}
>
<Pane main>
<Pane basis='250px' main>
<Loader suspense>
<SideBarPanel container={leftBarPanelService.tabsContainer} />
</Loader>
@@ -67,7 +67,7 @@ export const Main = observer(function Main() {
<RightArea />
</Pane>
<ResizerControls />
<Pane main>
<Pane basis='250px' main>
<Loader suspense>
<SideBarPanel container={sideBarPanelService.tabsContainer} />
</Loader>
@@ -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<React.PropsWithChildren<Props>> = function DisplayError({
@@ -54,12 +57,13 @@ export const DisplayError: React.FC<React.PropsWithChildren<Props>> = function D
error,
errorInfo,
className,
styles,
}) {
const stack = errorInfo?.componentStack || error?.stack;
return styled(style)(
<container className={className} {...use({ root })}>
<container-inner-block>
return styled(useStyles(style, styles))(
<error className={className} {...use({ root })}>
<error-inner-block>
<NotificationMark type={ENotificationType.Error} />
<p>Something went wrong.</p>
{root && <AppRefreshButton />}
@@ -71,7 +75,7 @@ export const DisplayError: React.FC<React.PropsWithChildren<Props>> = function D
{stack}
</details>
)}
</container-inner-block>
</container>
</error-inner-block>
</error>
);
};
@@ -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 && <action><Button onClick={onClose}>Close</Button></action>}
{this.canRefresh && <action><Button onClick={this.refresh}>Refresh</Button></action>}
</DisplayError>
);
@@ -97,7 +102,9 @@ export class ErrorBoundary
icon={icon}
className={className}
exception={errorData.error}
styles={styles}
onRetry={this.canRefresh ? this.refresh : undefined}
onClose={onClose}
/>
);
}
@@ -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<Props>(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<Props>(function ExceptionMessage({
};
}
return styled(styles)(
return styled(useStyles(style, styles))(
<error {...use({ inline, icon })} className={className}>
<error-icon title={message}>
<IconOrImage icon={inline ? '/icons/error_icon_sm.svg' : '/icons/error_icon.svg'} />
</error-icon>
{!icon && (
<error-data>
<error-name><span>{name}</span></error-name>
<error-message>{message}</error-message>
<error-actions>
{error.hasDetails && (
<Button type='button' mod={['outlined']} disabled={error.isOpen} onClick={error.open}>
{translate('ui_errors_details')}
</Button>
)}
{onRetry && (
<Button type='button' mod={['unelevated']} onClick={onRetry}>
{translate('ui_processing_retry')}
</Button>
)}
</error-actions>
</error-data>
<>
<error-data>
<error-name>
<span>{name}</span>
</error-name>
<error-message>{message}</error-message>
<error-actions>
{error.hasDetails && (
<Button type='button' mod={['outlined']} disabled={error.isOpen} onClick={error.open}>
{translate('ui_errors_details')}
</Button>
)}
{onRetry && (
<Button type='button' mod={['unelevated']} onClick={onRetry}>
{translate('ui_processing_retry')}
</Button>
)}
</error-actions>
</error-data>
{onClose && (
<error-action-close>
<Icon name="cross" viewBox="0 0 16 16" onClick={onClose} />
</error-action-close>
)}
</>
)}
</error>
);
@@ -44,7 +44,12 @@ export const FolderExplorerPath = observer<Props>(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(
<FolderName
@@ -10,8 +10,9 @@ import { css } from 'reshadow';
export const folderExplorerStyles = css`
folder-explorer-path {
display: flex;
composes: theme-typography--caption from global;
display: flex;
flex-wrap: wrap;
}
folder-explorer-path-element {
@@ -23,7 +24,6 @@ export const folderExplorerStyles = css`
width: 16px;
height: 16px;
transform: rotate(-90deg);
margin: 0 4px;
opacity: 0.5;
}
@@ -31,6 +31,7 @@ export const folderExplorerStyles = css`
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px;
}
folder-explorer-path-element:first-child folder-explorer-path-element-arrow {
@@ -14,8 +14,10 @@ import type { ComponentStyle } from '@cloudbeaver/core-theming';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import { Icon } from '../Icon';
import { Loader } from '../Loader/Loader';
import { useTranslate } from '../localization/useTranslate';
import { useCombinedHandler } from '../useCombinedHandler';
import { useStateDelay } from '../useStateDelay';
import { useStyles } from '../useStyles';
import { baseFormControlStyles, baseInvalidFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
import { FormContext } from './FormContext';
@@ -37,14 +39,16 @@ const INPUT_FIELD_STYLES = css`
input-container {
position: relative;
}
icon-container {
loader-container, icon-container {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
display: flex;
width: 24px;
height: 24px;
display: flex;
}
icon-container {
cursor: pointer;
& Icon {
width: 100%;
@@ -62,6 +66,7 @@ const INPUT_FIELD_STYLES = css`
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, '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 && (
<loader-container
title={translate('ui_processing_loading')}
>
<Loader small />
</loader-container>
)}
{showRevealPasswordButton && (
<icon-container
title={translate('ui_reveal_password')}
@@ -25,10 +25,10 @@ interface KeyWithIncludes<TKey, TIncludes> {
}
type ResourceData<
TResource extends CachedResource<any, any, any, any>,
TResource extends CachedResource<any, any, any, any, any>,
TKey,
TIncludes
> = TResource extends CachedDataResource<any, any, any>
> = TResource extends CachedDataResource<any, any, any, any>
? CachedResourceData<TResource>
: CachedMapResourceLoader<
TKey,
@@ -39,11 +39,12 @@ type ResourceData<
;
interface IActions<
TResource extends CachedResource<any, any, any, any>,
TResource extends CachedResource<any, any, any, any, any>,
TKey,
TIncludes
> {
active?: boolean;
forceSuspense?: boolean;
silent?: boolean;
onData?: (
data: ResourceData<TResource, TKey, TIncludes>,
@@ -110,7 +111,7 @@ type TResult<TResource, TKey, TIncludes> = (
);
export function useResource<
TResource extends CachedResource<any, any, any, any>,
TResource extends CachedResource<any, any, any, any, any>,
TKeyArg extends ResourceKey<CachedResourceKey<TResource>>,
TIncludes extends Readonly<CachedResourceContext<TResource>>
>(
@@ -121,7 +122,7 @@ export function useResource<
): TResult<TResource, TKeyArg, TIncludes>;
export function useResource<
TResource extends CachedResource<any, any, any, any>,
TResource extends CachedResource<any, any, any, any, any>,
TKeyArg extends ResourceKey<CachedResourceKey<TResource>>,
TIncludes extends CachedResourceContext<TResource>
>(
@@ -468,6 +469,10 @@ export function useResource<
}
}, [result.canLoad]);
if (actions?.forceSuspense) {
result.data;
}
return result;
}
+1 -1
View File
@@ -82,5 +82,5 @@ export function useStyles(
}
export function joinStyles(...styles: ComponentStyle[]): ComponentStyle {
return styles.flat();
return styles.flat(2);
}
+10 -6
View File
@@ -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<T extends Record<any, any>>(
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<T | null>(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;
}
@@ -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<IConnectionFolde
});
}
async create(key: IConnectionFolderParam, parentId?: string): Promise<ConnectionFolder> {
async create(projectId: string, name: string, parentId?: string): Promise<ConnectionFolder> {
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<IConnectionFolde
}
fromNodeId(nodeId: string): ConnectionFolder | undefined {
const data = /^folder:\/\/(.*?)\/(.*)$/ig.exec(nodeId);
const key = getConnectionFolderIdFromNodeId(nodeId);
if (data) {
const [t, projectId, folderId] = data;
return this.get(createConnectionFolderParam(projectId, folderId));
if (key) {
return this.get(key);
}
return undefined;
@@ -88,7 +90,7 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
): Promise<Map<IConnectionFolderParam, ConnectionFolder>> {
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<IConnectionFolde
projectId,
path: folderId,
});
folders.push(...folders);
folderList.push(...folders);
});
const key = resourceKeyList(folders.map<IConnectionFolderParam>(folder => createConnectionFolderParam(
const key = resourceKeyList(folderList.map<IConnectionFolderParam>(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<IConnectionFolde
this.delete(resourceKeyList(removedFolders));
}
this.set(key, folders);
this.set(key, folderList);
}
});
@@ -152,25 +154,3 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
);
}
}
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,
};
}
@@ -20,7 +20,7 @@ import { Connection, ConnectionInfoActiveProjectKey, ConnectionInfoResource, cre
import { ConnectionsManagerService } from '../ConnectionsManagerService';
import type { IConnectionInfoParams } from '../IConnectionsResource';
import { getConnectionParentId } from './getConnectionParentId';
import { getFolderNodeParents } from './getFolderParents';
import { getFolderNodeParents } from './getFolderNodeParents';
@injectable()
export class ConnectionNavNodeService extends Dependency {
@@ -57,9 +57,9 @@ 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.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
@@ -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;
}
@@ -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 [];
@@ -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,
};
}
@@ -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';
@@ -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;
}
}
`;
}
`];
@@ -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<TResult> {
id: string;
component: DialogComponent<any, any>;
payload: any;
options?: DialogOptions;
@@ -59,6 +61,7 @@ export class CommonDialogService {
});
const dialogInternal: DialogInternal<TResult> = {
id: uuid(),
component,
payload,
resolve: _resolve!,
@@ -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() {
<DialogBackdrop visible={!!activeDialog} onMouseDown={state.backdropClick}>
<inner-box>
{commonDialogService.dialogs.map((dialog, i, arr) => (
<NestedDialog
key={i}
visible={i === arr.length - 1}
dialog={dialog}
resolveDialog={state.resolve}
rejectDialog={state.reject}
/>
<ErrorBoundary
key={dialog.id}
styles={commonDialogContainerStyles}
remount
onClose={state.reject}
>
<NestedDialog
key={dialog.id}
visible={i === arr.length - 1}
dialog={dialog}
resolveDialog={state.resolve}
rejectDialog={state.reject}
/>
</ErrorBoundary>
))}
</inner-box>
</DialogBackdrop>
@@ -30,6 +30,10 @@ export const dialogStyles = css`
padding: 24px;
flex-direction: column;
align-items: center;
& > :not(:last-child) {
display: none;
}
}
Dialog {
@@ -411,10 +411,11 @@ export class NavTreeResource extends CachedMapResource<string, string[], Record<
if (!this.navNodeInfoResource.has(nodeId) && nodeId !== ROOT_NODE_PATH) {
await this.navNodeInfoResource.loadNodeParents(nodeId);
}
const preloaded = await this.preloadNodeParents(this.navNodeInfoResource.getParents(nodeId), nodeId);
const parents = this.navNodeInfoResource.getParents(nodeId);
const preloaded = await this.preloadNodeParents(parents, nodeId);
if (!preloaded) {
const cause = new DetailsError(`Entity not found: ${nodeId}`);
const cause = new DetailsError(`Entity not found:\n"${nodeId}"\nPath:\n${parents.map(parent => `"${parent}"`).join('\n')}`);
const error = new ResourceError(this, key, undefined, 'Entity not found', { cause });
ExecutorInterrupter.interrupt(contexts);
throw this.markError(error, key);
@@ -20,10 +20,12 @@ import { ResourceKeyListAlias, resourceKeyListAlias } from './ResourceKeyListAli
import { ResourceKeyUtils } from './ResourceKeyUtils';
export type CachedMapResourceKey<TResource> = CachedResourceKey<TResource>;
export type CachedMapResourceValue<TResource> = TResource extends CachedResource<Map<any, infer T>, any, any, any>
export type CachedMapResourceValue<TResource> = TResource extends CachedResource<Map<any, infer T>, any, any, any, any>
? T
: never;
export type CachedMapResourceArguments<TResource> = TResource extends CachedMapResource<any, any, infer T, any>
? T
: never;
export type CachedMapResourceArguments<TResource> = TResource extends CachedMapResource<any, any, infer T> ? T : never;
export type CachedMapResourceListGetter<
TValue,
@@ -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<TKey>, 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>): 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<TKey>): void {
if (!this.hasMetadata(key)) {
return;
}
this.updateMetadata(key, metadata => {
metadata.outdated = true;
});
@@ -12,7 +12,7 @@ import type { CachedResource } from './CachedResource';
export class ResourceError extends LoadingError {
constructor(
private readonly resource: CachedResource<any, any, any, any>,
private readonly resource: CachedResource<any, any, any, any, any>,
private readonly key: any,
private readonly context: any,
message?: string,
@@ -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<T extends Record<any, any> | Map<any, any>> implements ILocalS
subscribe() {
this.unsubscribe();
this.mobxSub = autorun(() => this.saveState());
this.mobxSub = reaction(
() => toJS(this.store),
() => this.saveState(),
{
fireImmediately: true,
delay: 500,
}
);
}
unsubscribe() {
@@ -12,4 +12,4 @@ export type BaseStyles = ClassCollection;
export type ThemeSelector = (theme: string) => Promise<undefined | BaseStyles | BaseStyles[]>;
export type Style = BaseStyles | ThemeSelector;
export type DynamicStyle = Style | boolean | undefined;
export type ComponentStyle = DynamicStyle | DynamicStyle[];
export type ComponentStyle = DynamicStyle | DynamicStyle[] | DynamicStyle[][];
+53 -49
View File
@@ -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<TKey, TValue> = (key: TKey, metadata: MetadataMap<TKey, any>) => TValue;
export type DefaultValueGetter<TKey, TValue> = (key: TKey, metadata: MetadataMap<TKey, TValue>) => TValue;
export class MetadataMap<TKey, TValue> {
private data: Map<TKey, TValue>;
private length: number;
export class MetadataMap<TKey, TValue> implements Map<TKey, TValue> {
get size(): number {
return this.temp.size;
}
private syncData: Array<[TKey, TValue]> | null;
private readonly temp: TempMap<TKey, TValue>;
private readonly data: Map<TKey, TValue>;
constructor(private readonly defaultValueGetter?: DefaultValueGetter<TKey, TValue>) {
this.data = observable(new Map());
this.length = 0;
this.syncData = null;
this.data = observable(new Map());
this.temp = new TempMap<TKey, TValue>(
this.data,
() => {
this.syncData?.splice(0, this.syncData.length, ...this.data.entries());
}
);
makeObservable<this, 'data'>(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<TKey, TValue>) => void, thisArg?: any): void {
this.temp.forEach(callbackfn, thisArg);
}
entries(): IterableIterator<[TKey, TValue]> {
return this.data.entries();
return this.temp.entries();
}
keys(): IterableIterator<TKey> {
return this.data.keys();
return this.temp.keys();
}
values(): IterableIterator<TValue> {
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<TKey, TValue>): 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);
}
}
+40 -9
View File
@@ -8,6 +8,8 @@
import { action } from 'mobx';
import { cacheValue, ICachedValueObject } from './cacheValue';
const combine = <T>(a: IterableIterator<T>, b: IterableIterator<T>): IterableIterator<T> => (
function* () { yield* a; yield* b; }
)();
@@ -28,23 +30,38 @@ export class TempMap<TKey, TValue> implements Map<TKey, TValue> {
private readonly deleted: TKey[];
private readonly temp: Map<TKey, TValue>;
private flushTask: NodeJS.Timeout | null;
private readonly keysTemp: ICachedValueObject<TKey[]>;
private readonly valuesTemp: ICachedValueObject<TValue[]>;
private readonly entriesTemp: ICachedValueObject<[TKey, TValue][]>;
constructor(
private readonly target: Map<TKey, TValue>
private readonly target: Map<TKey, TValue>,
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<TKey, TValue> implements Map<TKey, TValue> {
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<TKey> {
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<TValue> {
return Array.from(this.keys())
.map<TValue>(key => this.get(key)!)
return this.valuesTemp.value(() => Array.from(this.keys())
.map<TValue>(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<TKey, TValue> implements Map<TKey, TValue> {
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);
}
}
@@ -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<T> {
readonly invalid: boolean;
value(getter: () => T): T;
invalidate(): void;
}
export function cacheValue<T>(): ICachedValueObject<T> {
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;
},
};
}
+2
View File
@@ -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';
@@ -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');
}
@@ -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<any, {
projectId: string;
}> | 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) {
@@ -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,
};
}
@@ -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) => [
@@ -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',
});
@@ -131,7 +131,7 @@ export class NavigationTabsService extends View<ITab> {
this.onTabClose = new SyncExecutor();
this.onInit = new SyncExecutor();
this.onStateUpdate = new SyncExecutor();
this.metadata = new MetadataMap(() => ({ restored: false }));
this.metadata = new MetadataMap<string, ITabMetadata>(() => ({ restored: false }));
this.navigationTabContext = (): ITabNavigationContext => new TabNavigationContext(this, this.userInfoResource);
this.registerAction(ACTION_OPEN_IN_TAB);
@@ -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<ElementsTreeProps>(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<HTMLDivElement>(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<ElementsTreeProps>(function ElementsTree({
customSelect,
onExpand,
onSelect,
onOpen,
onClick,
});
const context = useMemo<IElementsTreeContext>(
@@ -230,24 +184,8 @@ export const ElementsTree = observer<ElementsTreeProps>(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<ElementsTreeProps>(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))(
<>
<ElementsTreeTools tree={tree} settingsElements={settingsElements} style={style} />
@@ -319,11 +241,9 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
</TreeNodeNested>
</drop-outside>
<ElementsTreeContentLoader
root={root}
context={context}
emptyPlaceholder={emptyPlaceholder}
childrenState={tree}
hasChildren={hasChildren}
>
<tree-elements>
<NavigationNodeNested
@@ -334,7 +254,6 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
root
/>
</tree-elements>
{loaderAvailable && <Loader state={tree} overlay={hasChildren} />}
</ElementsTreeContentLoader>
</tree>
</FolderExplorer>
@@ -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<React.PropsWithChildren<Props>>(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)(
<center>
<Loader />
</center>
);
}
const loading = getComputed(() => (
childrenState.isLoading()
|| context.tree.loading
) && !context.tree.isLoaded());
if (!hasChildren) {
if (loading) {
return styled(styles)(
<center>
<Loader />
</center>
return (
<Loader />
);
} else if (context.tree.filtering) {
return styled(TREE_NODE_STYLES)(
@@ -72,8 +50,8 @@ export const ElementsTreeContentLoader = observer<React.PropsWithChildren<Props>
}
return (
<>
<Loader suspense>
{children}
</>
</Loader>
);
});
@@ -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> | void;
onClick?: (node: NavNode, path: string[], leaf: boolean) => Promise<void> | void;
}
export const ElementsTreeContext = createContext<IElementsTreeContext | null>(null);
@@ -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<React.PropsWithChildren<Props>>(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)(
<tools>
@@ -45,7 +45,6 @@ export const NavigationNodeNested = observer(forwardRef<HTMLDivElement, Props>(f
!!root && treeContext?.folderExplorer.state.folder !== treeContext?.folderExplorer.root
));
if (!rootFolder) {
children = getComputed(
() => treeContext?.tree.getNodeChildren(nodeId) || [],
@@ -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,
@@ -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> | void;
onSelect?: (node: NavNode, state: boolean) => Promise<void> | void;
onExpand?: (node: NavNode, state: boolean) => Promise<void> | void;
onOpen?: (node: NavNode, folder: boolean) => Promise<void> | void;
onClick?: (node: NavNode) => Promise<void> | void;
isGroup?: (node: NavNode) => boolean;
customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void> | void;
beforeSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void> | void;
@@ -117,10 +119,13 @@ export interface IElementsTree extends ILoadableState {
select: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void>;
resetSelection(): Promise<void>;
setDnDData: (data: IDNDData, dragging: boolean) => void;
open: (node: NavNode, path: string[], leaf: boolean) => Promise<void>;
click: (node: NavNode, path: string[], leaf: boolean) => Promise<void>;
expand: (node: NavNode, state: boolean) => Promise<void>;
show: (nodeId: string, parents: string[]) => Promise<void>;
refresh: (nodeId: string) => Promise<void>;
collapse: () => void;
loadPath: (path: string[], lastNode?: string) => Promise<string | undefined>;
}
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<void> {
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<string | undefined> {
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;
}
@@ -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;
}
@@ -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)(
<CaptureView view={navTreeService}>
<ElementsTree
@@ -12,7 +12,7 @@ import { ConnectionInfoResource, ConnectionsManagerService, createConnectionPara
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { NavNodeManagerService, NavTreeResource, ROOT_NODE_PATH, EObjectFeature } from '@cloudbeaver/core-navigation-tree';
import { NavNodeManagerService, NavTreeResource, ROOT_NODE_PATH, EObjectFeature, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { ResourceKey, resourceKeyList } from '@cloudbeaver/core-sdk';
import { MetadataMap } from '@cloudbeaver/core-utils';
import { ACTION_COLLAPSE_ALL, ACTION_FILTER, IActiveView, View } from '@cloudbeaver/core-view';
@@ -36,11 +36,12 @@ export class NavigationTreeService extends View<string> {
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<string, ITreeNodeState>(() => ({
showInFilter: false,
expanded: false,
selected: false,
@@ -99,6 +100,12 @@ export class NavigationTreeService extends View<string> {
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) {
@@ -92,6 +92,10 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
function onDbClickHandler(event: React.MouseEvent<HTMLDivElement>) {
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<Na
<TreeNodeControl
ref={ref}
onClick={onClickHandler}
onDoubleClick={onDbClickHandler}
{...use({ outdated, editing, dragging: dndElement })}
className={className}
>
@@ -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<T = unknown> {
tab: ITab<IObjectViewerTabState>;
page: ObjectPage<T>;
onSelect: () => void;
style: DynamicStyle | DynamicStyle[];
style: ComponentStyle;
}
export type ObjectPageTabComponent<T = unknown> = React.FunctionComponent<ObjectPageTabProps<T>>;
@@ -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<void>;
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<FolderDialogPayload, IFolderDialogRes
const {
icon,
subTitle,
folder,
bigIcon,
viewBox,
value,
@@ -93,34 +98,55 @@ export const FolderDialog: DialogComponent<FolderDialogPayload, IFolderDialogRes
const state = useObservableRef<IFolderDialogState>(() => ({
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<FolderDialogPayload, IFolderDialogRes
}, [state.value, state.projectId]);
const errorMessage = state.valid ? ' ' : translate(state.message ?? 'ui_rename_taken_or_invalid');
const subTitle = createPath(projectInfoLoader.data?.name ?? state.projectId, state.folder);
return styled(style, BASE_CONTAINERS_STYLES)(
<CommonDialogWrapper size='small' className={className} fixedWidth>
@@ -154,6 +181,7 @@ export const FolderDialog: DialogComponent<FolderDialogPayload, IFolderDialogRes
state={state}
error={!state.valid}
description={errorMessage}
loading={state.validationInProgress}
onChange={() => state.validate()}
>
{translate('ui_name') + ':'}
@@ -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]));
});
}
}
+12 -11
View File
@@ -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
```