Feat CB-3293 node info transformer (#1778)

* CB-3293 feat: node info transformer

* CB-3293 feat: hide resource extension in nav-tree

* CB-3293 feat: rename resources without extension

* CB-3293 fix: resource folder renaming

* CB-3293 fix: add resource delete confirmation

* CB-3293 chore: remove unused code

* CB-3293 fix: resource deletion

* CB-2393 chore: unify delete confirmation dialog

---------

Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
Alexey
2023-06-27 13:46:55 +04:00
committed by GitHub
co-authored by dariamarutkina
parent 65718f681c
commit d4739a0dba
57 changed files with 542 additions and 139 deletions
@@ -0,0 +1,9 @@
/*
* 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 const NAV_NODE_TYPE_CONNECTION = 'Connection';
@@ -14,6 +14,7 @@ export * from './NavTree/getConnectionFolderIdFromNodeId';
export * from './NavTree/getConnectionFolderId';
export * from './NavTree/getConnectionParentId';
export * from './NavTree/getFolderNodeParents';
export * from './NavTree/NAV_NODE_TYPE_CONNECTION';
export * from './extensions/IConnectionProvider';
export * from './extensions/IConnectionSetter';
@@ -13,5 +13,12 @@ export const ConfirmationDialogDelete: DialogComponent<ConfirmationDialogPayload
payload,
...rest
}) {
return <ConfirmationDialog payload={{ ...payload, icon: '/icons/error_icon.svg', bigIcon: true }} {...rest} />;
const bigIcon = payload.bigIcon ?? payload.subTitle === undefined ?? true;
let icon = payload.icon ?? '/icons/error_icon_sm.svg';
if (bigIcon) {
icon = '/icons/error_icon.svg';
}
return <ConfirmationDialog payload={{ ...payload, icon, bigIcon }} {...rest} />;
};
@@ -17,6 +17,8 @@ import {
ProjectInfo as SchemaProjectInfo,
} from '@cloudbeaver/core-sdk';
import { createResourceOfType } from './createResourceOfType';
export type ProjectInfo = SchemaProjectInfo;
export type ProjectInfoResourceType = RmResourceType;
@@ -42,6 +44,37 @@ export class ProjectInfoResource extends CachedMapResource<string, ProjectInfo>
});
}
getNameWithoutExtension(projectId: string, resourceTypeId: string, fileName: string): string {
const project = this.get(projectId);
if (project) {
const resourceType = this.getResourceType(project, resourceTypeId);
for (let ext of resourceType?.fileExtensions || []) {
ext = `.${ext}`;
if (fileName.toLowerCase().endsWith(ext)) {
return fileName.slice(0, fileName.length - ext.length);
}
}
}
return fileName;
}
getNameWithExtension(projectId: string, resourceTypeId: string, fileName: string): string {
const project = this.get(projectId);
if (project) {
const resourceType = this.getResourceType(project, resourceTypeId);
if (resourceType?.fileExtensions.length) {
return createResourceOfType(resourceType, fileName);
}
}
return fileName;
}
getUserProject(userId: string): ProjectInfo | undefined {
return this.get(`u_${userId}`);
}
@@ -5,9 +5,14 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { isResourceOfType } from './isResourceOfType';
import type { ProjectInfoResourceType } from './ProjectInfoResource';
export function createResourceOfType(resourceType: ProjectInfoResourceType, name: string): string {
if (isResourceOfType(resourceType, name)) {
return name;
}
const extension = resourceType.fileExtensions[0];
return `${name}${extension ? '.' + extension : ''}`;
}
@@ -46,11 +46,11 @@ import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeav
import { FolderDialog } from '@cloudbeaver/plugin-projects';
import { NavResourceNodeService } from '../NavResourceNodeService';
import { ResourceManagerService } from '../ResourceManagerService';
import { DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID } from '../Tree/DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID';
import { getResourceKeyFromNodeId } from './getResourceKeyFromNodeId';
import { getResourceNodeId } from './getResourceNodeId';
import { getRmProjectNodeId } from './getRmProjectNodeId';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
interface ITargetNode {
projectId: string;
@@ -6,13 +6,20 @@
* you may not use this file except in compliance with the License.
*/
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ResourceManagerResource } from '@cloudbeaver/core-resource-manager';
import { getPathName } from '@cloudbeaver/core-utils';
import { getResourceNodeId } from './NavNodes/getResourceNodeId';
@injectable()
export class NavResourceNodeService {
constructor(private readonly resourceManagerResource: ResourceManagerResource) {}
constructor(
private readonly resourceManagerResource: ResourceManagerResource,
private readonly commonDialogService: CommonDialogService,
private readonly localizationService: LocalizationService,
) {}
async move(key: string, newKey: string): Promise<string> {
await this.resourceManagerResource.move(key, newKey);
@@ -21,6 +28,16 @@ export class NavResourceNodeService {
}
async delete(key: string) {
const result = await this.commonDialogService.open(ConfirmationDialogDelete, {
title: 'ui_data_delete_confirmation',
message: this.localizationService.translate('plugin_navigation_tree_rm_delete_confirmation_message', undefined, { name: getPathName(key) }),
confirmActionText: 'ui_delete',
});
if (result === DialogueStateResult.Rejected) {
return;
}
await this.resourceManagerResource.deleteResource(key);
}
@@ -10,16 +10,20 @@ import { CommonDialogService, DialogueStateResult, RenameDialog } from '@cloudbe
import { NotificationService } from '@cloudbeaver/core-events';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { DATA_CONTEXT_NAV_NODE } from '@cloudbeaver/core-navigation-tree';
import { isResourceOfType, ProjectInfoResource, ProjectInfoResourceType } from '@cloudbeaver/core-projects';
import { getRmResourceKey, NAV_NODE_TYPE_RM_FOLDER, NAV_NODE_TYPE_RM_RESOURCE } from '@cloudbeaver/core-resource-manager';
import { createPath, getPathParent } from '@cloudbeaver/core-utils';
import { ACTION_DELETE, ACTION_RENAME, ActionService } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_NAV_NODE_ACTIONS } from '@cloudbeaver/plugin-navigation-tree';
import { getResourceKeyFromNodeId, NavResourceNodeService } from '@cloudbeaver/plugin-resource-manager';
import { getResourceKeyFromNodeId } from './NavNodes/getResourceKeyFromNodeId';
import { NavResourceNodeService } from './NavResourceNodeService';
@injectable()
export class NavTreeRMContextMenuService extends Bootstrap {
constructor(
private readonly actionService: ActionService,
private readonly projectInfoResource: ProjectInfoResource,
private readonly commonDialogService: CommonDialogService,
private readonly notificationService: NotificationService,
private readonly navResourceNodeService: NavResourceNodeService,
@@ -53,6 +57,17 @@ export class NavTreeRMContextMenuService extends Bootstrap {
}
const key = getRmResourceKey(resourceKey);
const project = this.projectInfoResource.get(key.projectId);
let resourceType: ProjectInfoResourceType | undefined = undefined;
if (project) {
for (const type of project.resourceTypes) {
if (isResourceOfType(type, node.id)) {
resourceType = type;
break;
}
}
}
switch (action) {
case ACTION_RENAME: {
@@ -60,6 +75,10 @@ export class NavTreeRMContextMenuService extends Bootstrap {
const save = async (newName: string) => {
if (key.name !== newName && newName.trim().length) {
if (resourceType && node.nodeType === NAV_NODE_TYPE_RM_RESOURCE && !node.folder) {
newName = this.projectInfoResource.getNameWithExtension(key.projectId, resourceType.id, newName);
}
try {
await this.navResourceNodeService.move(resourceKey, createPath(getPathParent(resourceKey), newName));
node.name = newName; // fix name flickering in tree
@@ -25,8 +25,8 @@ import {
} from '@cloudbeaver/plugin-navigation-tree';
import { getRmProjectNodeId } from '../../NavNodes/getRmProjectNodeId';
import { ResourceManagerService } from '../../ResourceManagerService';
import { DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID } from '../DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
const styles = css`
TreeNodeControl {
@@ -11,8 +11,7 @@ import { NAV_NODE_TYPE_RM_PROJECT, RESOURCES_NODE_PATH } from '@cloudbeaver/core
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { createPath, isDefined } from '@cloudbeaver/core-utils';
import type { IElementsTreeFilter } from '@cloudbeaver/plugin-navigation-tree';
import type { ResourceManagerService } from '../../ResourceManagerService';
import type { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
export function navigationTreeProjectFilter(
projectsNavNodeService: ProjectsNavNodeService,
@@ -24,8 +24,8 @@ import {
useNode,
} from '@cloudbeaver/plugin-navigation-tree';
import type { ResourceManagerService } from '../../ResourceManagerService';
import { NavigationNodeProjectControl } from './NavigationNodeProjectControl';
import type { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
const nestedStyles = css`
TreeNode {
@@ -9,8 +9,7 @@ import type { NavNodeInfoResource, ProjectsNavNodeService } from '@cloudbeaver/c
import { RESOURCES_NODE_PATH } from '@cloudbeaver/core-resource-manager';
import { createPath } from '@cloudbeaver/core-utils';
import type { IElementsTreeNodeExpandInfoGetter } from '@cloudbeaver/plugin-navigation-tree';
import type { ResourceManagerService } from '../../ResourceManagerService';
import type { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
export function navigationTreeResourceExpandStateGetter(
navNodeInfoResource: NavNodeInfoResource,
@@ -26,7 +26,6 @@ import {
validateElementsTreeSettings,
} from '@cloudbeaver/plugin-navigation-tree';
import { ResourceManagerService } from '../ResourceManagerService';
import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter';
import { navigationTreeProjectSearchCompare } from './ProjectsRenderer/navigationTreeProjectSearchCompare';
import { navigationTreeProjectsExpandStateGetter } from './ProjectsRenderer/navigationTreeProjectsExpandStateGetter';
@@ -35,6 +34,8 @@ import { navigationTreeResourceTypeFilter } from './ProjectsRenderer/navigationT
import { ProjectsSettingsPlaceholderElement } from './ProjectsRenderer/ProjectsSettingsForm';
import { navigationTreeResourceExpandStateGetter } from './ResourceFolderRenderer/navigationTreeResourceExpandStateGetter';
import { ResourceManagerTreeCaptureViewContext } from './ResourceManagerTreeCaptureViewContext';
import { transformResourceNodeInfo } from './ResourceRenderer/transformResourceNodeInfo';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
const styles = css`
CaptureView {
@@ -107,6 +108,10 @@ export const ResourceManagerTree: React.FC<Props> = observer(function ResourceMa
() => navigationTreeProjectsExpandStateGetter(navNodeInfoResource, projectsService, projectsNavNodeService),
[navNodeInfoResource, projectsService, projectsNavNodeService],
);
const transformResourceNode = useMemo(
() => transformResourceNodeInfo(projectInfoResource, projectsNavNodeService, navNodeInfoResource, resourceTypeId),
[projectInfoResource, projectsNavNodeService, navNodeInfoResource, resourceTypeId],
);
const projectFilter = useMemo(
() =>
navigationTreeProjectFilter(
@@ -134,6 +139,7 @@ export const ResourceManagerTree: React.FC<Props> = observer(function ResourceMa
getChildren={navTreeService.getChildren}
loadChildren={navTreeService.loadNestedNodes}
settings={settings}
nodeInfoTransformers={[transformResourceNode]}
filters={[resourceTypeFilter, projectFilter]}
renderers={[projectsRendererRenderer]}
expandStateGetters={[projectsExpandStateGetter, resourceExpandStateGetter]}
@@ -0,0 +1,33 @@
/*
* 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 { NavNodeInfoResource, ProjectsNavNodeService } from '@cloudbeaver/core-navigation-tree';
import type { ProjectInfoResource } from '@cloudbeaver/core-projects';
import { NAV_NODE_TYPE_RM_RESOURCE } from '@cloudbeaver/core-resource-manager';
import type { IElementsTreeCustomNodeInfo } from '@cloudbeaver/plugin-navigation-tree';
export function transformResourceNodeInfo(
projectInfoResource: ProjectInfoResource,
projectsNavNodeService: ProjectsNavNodeService,
navNodeInfoResource: NavNodeInfoResource,
resourceTypeId: string | undefined,
): IElementsTreeCustomNodeInfo {
return function transformResourceNodeInfo(nodeId, info) {
const node = navNodeInfoResource.get(nodeId);
if (node?.nodeType === NAV_NODE_TYPE_RM_RESOURCE && resourceTypeId) {
const project = projectsNavNodeService.getProject(node.id);
if (project) {
return {
...info,
name: projectInfoResource.getNameWithoutExtension(project.id, resourceTypeId, info.name),
};
}
}
return info;
};
}
@@ -1,4 +1,9 @@
import { navigationTreeRMPlugin } from './manifest';
export * from './NavNodes/getResourceKeyFromNodeId';
export * from './NavNodes/getResourceNodeId';
export * from './NavNodes/getRmProjectNodeId';
export * from './Tree/ResourceManagerTree';
export * from './NavResourceNodeService';
export { navigationTreeRMPlugin };
export default navigationTreeRMPlugin;
@@ -1 +1 @@
export default [];
export default [['plugin_navigation_tree_rm_delete_confirmation_message', 'You\'re going to delete "{arg:name}" resource. Are you sure?']];
@@ -1 +1 @@
export default [];
export default [['plugin_navigation_tree_rm_delete_confirmation_message', 'You\'re going to delete "{arg:name}" resource. Are you sure?']];
@@ -1 +1 @@
export default [];
export default [['plugin_navigation_tree_rm_delete_confirmation_message', 'Вы собираетесь удалить "{arg:name}" ресурс. Вы уверены?']];
@@ -1 +1 @@
export default [];
export default [['plugin_navigation_tree_rm_delete_confirmation_message', 'You\'re going to delete "{arg:name}" resource. Are you sure?']];
@@ -9,8 +9,15 @@ import type { PluginManifest } from '@cloudbeaver/core-di';
import { LocaleService } from './LocaleService';
import { NavTreeRMContextMenuService } from './NavTreeRMContextMenuService';
import { ResourceFoldersBootstrap } from './NavNodes/ResourceFoldersBootstrap';
import { NavResourceNodeService } from './NavResourceNodeService';
export const navigationTreeRMPlugin: PluginManifest = {
info: { name: 'Navigation Tree RM plugin' },
providers: [LocaleService, NavTreeRMContextMenuService],
providers: [
LocaleService,
NavResourceNodeService,
ResourceFoldersBootstrap,
NavTreeRMContextMenuService
],
};
@@ -0,0 +1,83 @@
/*
* 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 { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext } from 'react';
import styled, { use } from 'reshadow';
import {
ConnectionImageWithMask,
getComputed,
Loader,
TREE_NODE_STYLES,
TreeNodeContext,
TreeNodeControl,
TreeNodeIcon,
TreeNodeName,
useMouseContextMenu,
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { EObjectFeature, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import type { NavTreeControlComponent, NavTreeControlProps } from '../ElementsTree/NavigationNodeComponent';
import { NAVIGATION_NODE_CONTROL_STYLES } from '../ElementsTree/NavigationTreeNode/NavigationNode/NAVIGATION_NODE_CONTROL_STYLES';
import { NavigationNodeExpand } from '../ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeExpand';
import { TreeNodeMenuLoader } from '../ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenuLoader';
export const ConnectionNavNodeControl: NavTreeControlComponent = observer<NavTreeControlProps, HTMLDivElement>(
forwardRef(function ConnectionNavNodeControl({ node, nodeInfo, dndElement, dndPlaceholder, className, onClick }, ref) {
const mouseContextMenu = useMouseContextMenu();
const treeNodeContext = useContext(TreeNodeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
const navTreeResource = useService(NavTreeResource);
const selected = treeNodeContext.selected;
const error = getComputed(() => !!navNodeInfoResource.getException(node.id) || !!navTreeResource.getException(node.id));
const connected = getComputed(() => node.objectFeatures.includes(EObjectFeature.dataSourceConnected));
let icon = nodeInfo.icon;
const name = nodeInfo.name;
const title = nodeInfo.name;
if (error) {
icon = '/icons/error_icon_sm.svg';
}
function handlePortalClick(event: React.MouseEvent<HTMLDivElement>) {
EventContext.set(event, EventStopPropagationFlag);
treeNodeContext.select();
}
function handleContextMenuOpen(event: React.MouseEvent<HTMLDivElement>) {
mouseContextMenu.handleContextMenuOpen(event);
treeNodeContext.select();
}
return styled(
TREE_NODE_STYLES,
NAVIGATION_NODE_CONTROL_STYLES,
)(
<TreeNodeControl ref={ref} className={className} onClick={onClick} onContextMenu={handleContextMenuOpen} {...use({ dragging: dndElement })}>
<NavigationNodeExpand nodeId={node.id} />
<TreeNodeIcon {...use({ connected })}>
<ConnectionImageWithMask icon={icon} connected={connected} maskId="tree-node-icon" />
</TreeNodeIcon>
<TreeNodeName title={title}>
<Loader suspense inline fullSize>
<name-box>{name}</name-box>
</Loader>
</TreeNodeName>
{!dndPlaceholder && (
<portal onClick={handlePortalClick}>
<TreeNodeMenuLoader mouseContextMenu={mouseContextMenu} node={node} selected={selected} />
</portal>
)}
</TreeNodeControl>,
);
}),
);
@@ -0,0 +1,63 @@
/*
* 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 { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { Translate, TREE_NODE_STYLES, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks';
import { NAV_NODE_TYPE_CONNECTION } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent';
import { NavigationNodeRendererLoader } from '../ElementsTree/NavigationTreeNode/NavigationNodeRendererLoader';
import type { IElementsTreeCustomRenderer } from '../ElementsTree/useElementsTree';
import { ConnectionNavNodeControl } from './ConnectionNavNodeControl';
export function navTreeConnectionRenderer(navNodeInfoResource: NavNodeInfoResource): IElementsTreeCustomRenderer {
return nodeId => {
const node = navNodeInfoResource.get(nodeId);
if (node?.nodeType === NAV_NODE_TYPE_CONNECTION) {
return ConnectionRenderer;
}
return undefined;
};
}
const ConnectionRenderer: NavigationNodeRendererComponent = observer(function ManageableGroup({
nodeId,
path,
dragging,
component,
className,
expanded,
}) {
const navNodeInfoResource = useService(NavNodeInfoResource);
const node = navNodeInfoResource.get(nodeId);
if (!node) {
return styled(TREE_NODE_STYLES)(
<TreeNodeNestedMessage>
<Translate token="app_navigationTree_node_not_found" />
</TreeNodeNestedMessage>,
);
}
return (
<NavigationNodeRendererLoader
node={node}
path={path}
expanded={expanded}
dragging={dragging}
className={className}
control={ConnectionNavNodeControl}
component={component}
/>
);
});
@@ -129,6 +129,7 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
selectionTree = false,
emptyPlaceholder,
navNodeFilterCompare,
nodeInfoTransformers = [],
filters = [],
renderers = [],
expandStateGetters,
@@ -172,6 +173,7 @@ export const ElementsTree = observer<ElementsTreeProps>(function ElementsTree({
root,
disabled,
localState,
nodeInfoTransformers,
filters: [nameFilter, ...filters, limitFilter],
renderers: [...renderers, elementsTreeLimitRenderer],
expandStateGetters,
@@ -0,0 +1,13 @@
/*
* 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 INavTreeNodeInfo {
name: string;
tooltip?: string;
icon?: string;
}
@@ -10,6 +10,8 @@ import type React from 'react';
import type { NavNode } from '@cloudbeaver/core-navigation-tree';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import type { INavTreeNodeInfo } from './INavTreeNodeInfo';
export type NavTreeNodeComponent = React.FC<{
nodeId: string;
path: string[];
@@ -20,12 +22,14 @@ export type NavTreeNodeComponent = React.FC<{
export type NavTreeControlProps = {
node: NavNode;
nodeInfo: INavTreeNodeInfo;
dndElement?: boolean;
dndPlaceholder?: boolean;
expanded?: boolean;
className?: string;
style?: ComponentStyle;
ref?: React.Ref<HTMLDivElement> | undefined;
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
};
export type NavTreeControlComponent = React.FC<NavTreeControlProps>;
@@ -0,0 +1,39 @@
/*
* 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 { css } from 'reshadow';
export const NAVIGATION_NODE_CONTROL_STYLES = css`
TreeNodeControl:hover > portal,
TreeNodeControl:global([aria-selected='true']) > portal,
portal:focus-within {
visibility: visible;
}
TreeNodeName {
height: 100%;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
&[|editing] {
padding: 0;
overflow: visible;
margin-left: 2px;
}
}
portal {
position: relative;
box-sizing: border-box;
margin-left: auto !important;
margin-right: 8px !important;
visibility: hidden;
}
name-box {
overflow: hidden;
text-overflow: ellipsis;
}
`;
@@ -7,17 +7,15 @@
*/
import { action, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext, useDeferredValue } from 'react';
import styled, { css, use } from 'reshadow';
import React, { forwardRef, useContext } from 'react';
import styled, { use } from 'reshadow';
import {
ConnectionImageWithMask,
getComputed,
Loader,
TREE_NODE_STYLES,
TreeNodeContext,
TreeNodeControl,
TreeNodeExpand,
TreeNodeIcon,
TreeNodeName,
useMouseContextMenu,
@@ -26,53 +24,15 @@ import {
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { EObjectFeature, type INodeActions, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import { type INodeActions, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import { ElementsTreeContext } from '../../ElementsTreeContext';
import type { NavTreeControlComponent, NavTreeControlProps } from '../../NavigationNodeComponent';
import { TreeNodeMenuLoader } from '../TreeNodeMenu/TreeNodeMenuLoader';
import { DATA_ATTRIBUTE_NODE_EDITING } from './DATA_ATTRIBUTE_NODE_EDITING';
import { NAVIGATION_NODE_CONTROL_STYLES } from './NAVIGATION_NODE_CONTROL_STYLES';
import { NavigationNodeExpand } from './NavigationNodeExpand';
import { NavigationNodeEditorLoader } from './NavigationNodeLoaders';
const styles = css`
TreeNodeControl {
transition: opacity 0.3s ease;
opacity: 1;
&[|outdated] {
opacity: 0.5;
}
}
TreeNodeControl:hover > portal,
TreeNodeControl:global([aria-selected='true']) > portal,
portal:focus-within {
visibility: visible;
}
TreeNodeName {
height: 100%;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
&[|editing] {
padding: 0;
overflow: visible;
margin-left: 2px;
}
}
portal {
position: relative;
box-sizing: border-box;
margin-left: auto !important;
margin-right: 8px !important;
visibility: hidden;
}
name-box {
overflow: hidden;
text-overflow: ellipsis;
}
`;
interface IEditingState {
saving: boolean;
editing: boolean;
@@ -88,17 +48,13 @@ interface IEditingState {
}
export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeControlProps, HTMLDivElement>(
forwardRef(function NavigationNodeControl({ node, dndElement, dndPlaceholder }, ref) {
forwardRef(function NavigationNodeControl({ node, nodeInfo, dndElement, dndPlaceholder, className, onClick }, ref) {
const mouseContextMenu = useMouseContextMenu();
const treeNodeContext = useContext(TreeNodeContext);
const treeContext = useContext(ElementsTreeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
const navTreeResource = useService(NavTreeResource);
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading);
const error = getComputed(() => !!navNodeInfoResource.getException(node.id) || !!navTreeResource.getException(node.id));
const connected = getComputed(() => node.objectFeatures.includes(EObjectFeature.dataSourceConnected));
const selected = treeNodeContext.selected;
const editingState = useObservableRef<IEditingState>(
() => ({
saving: false,
@@ -151,7 +107,9 @@ export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeCo
rename: editingState.startEditing,
});
let icon = node.icon;
let icon = nodeInfo.icon;
const name = nodeInfo.name;
const title = nodeInfo.tooltip;
if (error) {
icon = '/icons/error_icon_sm.svg';
@@ -162,42 +120,35 @@ export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeCo
treeNodeContext.select();
}
function onClickHandler(event: React.MouseEvent<HTMLDivElement>) {
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
function handleContextMenuOpen(event: React.MouseEvent<HTMLDivElement>) {
mouseContextMenu.handleContextMenuOpen(event);
treeNodeContext.select();
}
const expandable = useDeferredValue(getComputed(() => treeContext?.tree.isNodeExpandable(node.id) ?? true));
const filterActive = useDeferredValue(getComputed(() => treeContext?.tree.filtering));
const { editing, saving } = editingState;
const attributes = { [DATA_ATTRIBUTE_NODE_EDITING]: editing };
return styled(
TREE_NODE_STYLES,
styles,
NAVIGATION_NODE_CONTROL_STYLES,
)(
<TreeNodeControl
ref={ref}
{...attributes}
onClick={onClickHandler}
className={className}
onClick={onClick}
onContextMenu={handleContextMenuOpen}
{...use({ outdated, editing, dragging: dndElement })}
{...use({ editing, dragging: dndElement })}
>
{expandable && <TreeNodeExpand filterActive={filterActive} />}
<TreeNodeIcon {...use({ connected })}>
<ConnectionImageWithMask icon={icon} connected={connected} maskId="tree-node-icon" />
</TreeNodeIcon>
<TreeNodeName title={node.name} {...use({ editing })}>
<NavigationNodeExpand nodeId={node.id} />
<TreeNodeIcon icon={icon} />
<TreeNodeName title={title} {...use({ editing })}>
<Loader suspense inline fullSize>
{editing ? (
<NavigationNodeEditorLoader node={node} disabled={saving} onSave={editingState.save} onClose={editingState.cancel} />
<NavigationNodeEditorLoader name={name} disabled={saving} onSave={editingState.save} onClose={editingState.cancel} />
) : (
<name-box>{node.name}</name-box>
<name-box>{name}</name-box>
)}
</Loader>
</TreeNodeName>
@@ -10,28 +10,29 @@ import { useState } from 'react';
import styled, { css } from 'reshadow';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import type { NavNode } from '@cloudbeaver/core-navigation-tree';
import { InlineEditor } from '@cloudbeaver/core-ui';
const styles = css`
InlineEditor {
height: 22px;
& input {
& input,
& input[disabled],
& input[readonly] {
padding: 1px;
}
}
`;
interface Props {
node: NavNode;
name: string;
disabled?: boolean;
onSave: (name: string) => void;
onClose: () => void;
}
export const NavigationNodeEditor = observer<Props>(function NavigationNodeEditor({ node, disabled, onSave, onClose }) {
const [name, setName] = useState(node.name || '');
export const NavigationNodeEditor = observer<Props>(function NavigationNodeEditor({ name: initialName, disabled, onSave, onClose }) {
const [name, setName] = useState(initialName);
function save() {
onSave(name);
@@ -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 { observer } from 'mobx-react-lite';
import { useContext, useDeferredValue } from 'react';
import styled from 'reshadow';
import { getComputed, TREE_NODE_STYLES, TreeNodeExpand } from '@cloudbeaver/core-blocks';
import { ElementsTreeContext } from '../../ElementsTreeContext';
interface Props {
nodeId: string;
}
export const NavigationNodeExpand = observer<Props>(function NavigationNodeExpand({ nodeId }) {
const treeContext = useContext(ElementsTreeContext);
const expandable = useDeferredValue(getComputed(() => treeContext?.tree.isNodeExpandable(nodeId) ?? true));
const filterActive = useDeferredValue(getComputed(() => treeContext?.tree.filtering ?? false));
if (!expandable) {
return null;
}
return styled(TREE_NODE_STYLES)(<TreeNodeExpand filterActive={filterActive} />);
});
@@ -7,13 +7,16 @@
*/
import { observer } from 'mobx-react-lite';
import { forwardRef, useContext, useDeferredValue, useEffect, useRef, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { useObjectRef } from '@cloudbeaver/core-blocks';
import type { NavNode } from '@cloudbeaver/core-navigation-tree';
import { getComputed, TreeNodeContext, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { type NavNode, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { ElementsTreeContext } from '../ElementsTreeContext';
import type { NavTreeControlComponent } from '../NavigationNodeComponent';
import { transformNodeInfo } from '../transformNodeInfo';
import { NavigationNodeControlLoader } from './NavigationNode/NavigationNodeLoaders';
import type { INavigationNode } from './useNavigationNode';
@@ -25,6 +28,17 @@ interface Props {
style?: ComponentStyle;
}
const styles = css`
Control {
transition: opacity 0.3s ease;
opacity: 1;
&[|outdated] {
opacity: 0.5;
}
}
`;
export const NavigationNodeControlRenderer = observer<Props, HTMLDivElement>(
forwardRef(function NavigationNodeControlRenderer({ node, navNode, dragging, control: externalControl, style }, ref) {
const elementRef = useRef<HTMLDivElement | null>(null);
@@ -32,7 +46,8 @@ export const NavigationNodeControlRenderer = observer<Props, HTMLDivElement>(
const contextRef = useObjectRef({
context: useContext(ElementsTreeContext),
});
const Control = navNode.control || externalControl || NavigationNodeControlLoader;
const treeNodeContext = useContext(TreeNodeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
const observer = useRef<IntersectionObserver | null>(null);
useEffect(() => {
@@ -77,6 +92,16 @@ export const NavigationNodeControlRenderer = observer<Props, HTMLDivElement>(
return <div ref={setRef} style={{ height: `${size}px` }} />;
}
return <Control ref={setRef} dndElement={dragging} style={style} node={node} />;
const Control = navNode.control || externalControl || NavigationNodeControlLoader;
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading);
const nodeInfo = transformNodeInfo(node, contextRef.context?.tree.nodeInfoTransformers ?? []);
function onClickHandler(event: React.MouseEvent<HTMLDivElement>) {
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
return styled(styles)(
<Control ref={setRef} {...use({ outdated })} nodeInfo={nodeInfo} dndElement={dragging} style={style} node={node} onClick={onClickHandler} />,
);
}),
);
@@ -13,6 +13,7 @@ import { getComputed, TreeNode, useObjectRef, useStyles } from '@cloudbeaver/cor
import { ElementsTreeContext } from '../ElementsTreeContext';
import type { NavigationNodeComponent } from '../NavigationNodeComponent';
import { transformNodeInfo } from '../transformNodeInfo';
import { NavigationNodeControlLoader } from './NavigationNode/NavigationNodeLoaders';
export const NavigationNodeDragged: NavigationNodeComponent = observer(function NavigationNode({ node, className, control: externalControl, style }) {
@@ -22,11 +23,12 @@ export const NavigationNodeDragged: NavigationNodeComponent = observer(function
const control = getComputed(() => contextRef.context?.control);
const Control = control || externalControl || NavigationNodeControlLoader;
const nodeInfo = transformNodeInfo(node, contextRef.context?.tree.nodeInfoTransformers ?? []);
return styled(useStyles(style))(
<TreeNode externalExpanded={false} className={className} leaf>
{/* <DNDPreview data={dndData} src="/icons/empty.svg" /> */}
<Control node={node} dndPlaceholder />
<Control nodeInfo={nodeInfo} node={node} dndPlaceholder />
</TreeNode>,
);
});
@@ -0,0 +1,19 @@
/*
* 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 { NavNode } from '@cloudbeaver/core-navigation-tree';
import type { INavTreeNodeInfo } from './INavTreeNodeInfo';
import type { IElementsTreeCustomNodeInfo } from './useElementsTree';
export function transformNodeInfo(node: NavNode, transformers: IElementsTreeCustomNodeInfo[]): INavTreeNodeInfo {
return transformers.reduce((info, transformer) => transformer(node.id, info), {
name: node.name,
tooltip: node.name,
icon: node.icon,
} as INavTreeNodeInfo);
}
@@ -20,9 +20,11 @@ import type { IDNDData } from '@cloudbeaver/core-ui';
import { ILoadableState, MetadataMap, throttle } from '@cloudbeaver/core-utils';
import type { IElementsTreeAction } from './IElementsTreeAction';
import type { INavTreeNodeInfo } from './INavTreeNodeInfo';
import type { NavigationNodeRendererComponent } from './NavigationNodeComponent';
export type IElementsTreeCustomRenderer = (nodeId: string) => NavigationNodeRendererComponent | undefined;
export type IElementsTreeCustomNodeInfo = (nodeId: string, info: INavTreeNodeInfo) => INavTreeNodeInfo;
export interface IElementsTreeNodeExpandedInfo {
expanded: boolean;
@@ -66,6 +68,7 @@ export interface IElementsTreeOptions {
disabled?: boolean;
filters?: IElementsTreeFilter[];
renderers?: IElementsTreeCustomRenderer[];
nodeInfoTransformers?: IElementsTreeCustomNodeInfo[];
expandStateGetters?: IElementsTreeNodeExpandInfoGetter[];
localState?: MetadataMap<string, ITreeNodeState>;
getChildren: (id: string) => string[] | undefined;
@@ -97,6 +100,7 @@ export interface IElementsTree extends ILoadableState {
loading: boolean;
disabled: boolean;
activeDnDData: IDNDData[];
nodeInfoTransformers: IElementsTreeCustomNodeInfo[];
renderers: IElementsTreeCustomRenderer[];
state: MetadataMap<string, ITreeNodeState>;
userData: IElementsTreeUserState;
@@ -141,6 +145,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
);
options = useObjectRef(options);
options.nodeInfoTransformers = useMemo(() => options.nodeInfoTransformers || [], [...(options.nodeInfoTransformers || [])]);
options.renderers = useMemo(() => options.renderers || [], [...(options.renderers || [])]);
options.filters = useMemo(() => options.filters || [], [...(options.filters || [])]);
options.expandStateGetters = useMemo(() => options.expandStateGetters || [], [...(options.expandStateGetters || [])]);
@@ -627,6 +632,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
filtering: computed,
loading: observable.ref,
renderers: observable.ref,
nodeInfoTransformers: observable.ref,
baseRoot: observable.ref,
collapse: action.bound,
userData: observable.ref,
@@ -639,6 +645,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
settings: options.settings,
baseRoot: options.baseRoot,
renderers: options.renderers,
nodeInfoTransformers: options.nodeInfoTransformers,
userData,
},
['isLoading', 'isLoaded'],
@@ -9,7 +9,6 @@ import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import styled, { css } from 'reshadow';
import { AppAuthService } from '@cloudbeaver/core-authentication';
import { Translate, useUserData } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NavNodeInfoResource, NavTreeResource, ProjectsNavNodeService, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
@@ -19,6 +18,7 @@ import { CaptureView } from '@cloudbeaver/core-view';
import { NavNodeViewService } from '../NodesManager/NavNodeView/NavNodeViewService';
import { navigationTreeConnectionGroupFilter } from './ConnectionGroup/navigationTreeConnectionGroupFilter';
import { navigationTreeConnectionGroupRenderer } from './ConnectionGroup/navigationTreeConnectionGroupRenderer';
import { navTreeConnectionRenderer } from './ConnectionsRenderer/navTreeConnectionRenderer';
import { ElementsTree } from './ElementsTree/ElementsTree';
import {
createElementsTreeSettings,
@@ -98,6 +98,7 @@ export const NavigationTree = observer(function NavigationTree() {
);
const duplicateFilter = useMemo(() => navigationTreeDuplicateFilter(navNodeViewService), [navNodeViewService]);
const connectionRenderer = useMemo(() => navTreeConnectionRenderer(navNodeInfoResource), [navNodeInfoResource]);
const projectsRendererRenderer = useMemo(() => navigationTreeProjectsRendererRenderer(navNodeInfoResource), [navNodeInfoResource]);
const projectsExpandStateGetter = useMemo(
() => navigationTreeProjectsExpandStateGetter(navNodeInfoResource, projectsService, projectsNavNodeService),
@@ -116,7 +117,7 @@ export const NavigationTree = observer(function NavigationTree() {
root={root}
localState={navTreeService.treeState}
filters={[duplicateFilter, connectionGroupFilter, projectFilter]}
renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer]}
renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer, connectionRenderer]}
navNodeFilterCompare={navigationTreeProjectSearchCompare}
expandStateGetters={[projectsExpandStateGetter]}
settingsElements={settingsElements}
@@ -16,6 +16,7 @@
"validate-dependencies": "core-cli-validate-dependencies"
},
"dependencies": {
"@cloudbeaver/plugin-navigation-tree-rm": "~0.1.0",
"@cloudbeaver/plugin-resource-manager": "~0.1.0",
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-root": "~0.1.0",
@@ -10,7 +10,7 @@ import styled, { css } from 'reshadow';
import { useTranslate } from '@cloudbeaver/core-blocks';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import { ResourceManagerTree } from '@cloudbeaver/plugin-resource-manager';
import { ResourceManagerTree } from '@cloudbeaver/plugin-navigation-tree-rm';
import { SCRIPTS_TYPE_ID } from './SCRIPTS_TYPE_ID';
@@ -3,10 +3,5 @@ import { resourceManagerPlugin } from './manifest';
export default resourceManagerPlugin;
export * from './ResourceManagerSettingsService';
export * from './NavResourceNodeService';
export * from './RESOURCE_NAME_REGEX';
export * from './ResourceManagerService';
export * from './NavNodes/getResourceKeyFromNodeId';
export * from './NavNodes/getResourceNodeId';
export * from './NavNodes/getRmProjectNodeId';
export * from './Tree/ResourceManagerTree';
@@ -8,8 +8,6 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { LocaleService } from './LocaleService';
import { ResourceFoldersBootstrap } from './NavNodes/ResourceFoldersBootstrap';
import { NavResourceNodeService } from './NavResourceNodeService';
import { PluginBootstrap } from './PluginBootstrap';
import { ResourceManagerService } from './ResourceManagerService';
import { ResourceManagerSettingsService } from './ResourceManagerSettingsService';
@@ -23,8 +21,6 @@ export const resourceManagerPlugin: PluginManifest = {
PluginBootstrap,
LocaleService,
ResourceManagerService,
NavResourceNodeService,
ResourceFoldersBootstrap,
// ResourceProjectsResource,
],
};
@@ -20,6 +20,7 @@
"@cloudbeaver/plugin-resource-manager": "~0.1.0",
"@cloudbeaver/plugin-sql-editor": "~0.1.0",
"@cloudbeaver/plugin-sql-editor-navigation-tab": "~0.1.0",
"@cloudbeaver/plugin-navigation-tree-rm": "~0.1.0",
"@cloudbeaver/core-connections": "~0.1.0",
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-dialogs": "~0.1.0",
@@ -10,13 +10,14 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial
import { NotificationService } from '@cloudbeaver/core-events';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { type INodeNavigationData, NavNodeInfoResource, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { createResourceOfType, isResourceOfType, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
import { isResourceOfType, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
import { NAV_NODE_TYPE_RM_RESOURCE, ResourceManagerResource, RESOURCES_NODE_PATH } from '@cloudbeaver/core-resource-manager';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { createPath, getPathName } from '@cloudbeaver/core-utils';
import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view';
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
import { getResourceKeyFromNodeId, NavResourceNodeService, ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
import { getResourceKeyFromNodeId, NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
import { ResourceManagerScriptsService, SaveScriptDialog, SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
import {
DATA_CONTEXT_SQL_EDITOR_STATE,
@@ -123,12 +124,7 @@ export class PluginBootstrap extends Bootstrap {
throw new Error('Project not found');
}
const resourceType = this.projectInfoResource.getResourceType(project, SCRIPTS_TYPE_ID);
if (!resourceType) {
throw new Error('Script Resource type not found');
}
const scriptName = createResourceOfType(resourceType, result.name.trim());
const scriptName = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, result.name.trim());
const scriptsRootFolder = this.resourceManagerScriptsService.getRootFolder(project);
const folderResourceKey = getResourceKeyFromNodeId(createPath(RESOURCES_NODE_PATH, projectId, scriptsRootFolder));
@@ -14,9 +14,11 @@ import {
NOT_INITIALIZED_CONTEXT_ID,
} from '@cloudbeaver/core-connections';
import { TaskScheduler } from '@cloudbeaver/core-executor';
import type { ProjectInfoResource } from '@cloudbeaver/core-projects';
import { getRmResourceKey, ResourceManagerResource } from '@cloudbeaver/core-resource-manager';
import { isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { createPath, debounce, getPathName, getPathParent, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
import { BaseSqlDataSource, ESqlDataSourceFeatures } from '@cloudbeaver/plugin-sql-editor';
import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState';
@@ -43,13 +45,13 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
static key = 'resource';
get name(): string | null {
if (!this.resourceKey) {
if (!this.resourceKey || !this.projectId) {
return null;
}
const resource = this.resourceManagerResource.get(this.resourceKey);
return resource?.name ?? getPathName(this.resourceKey);
return this.projectInfoResource.getNameWithoutExtension(this.projectId, SCRIPTS_TYPE_ID, resource?.name ?? getPathName(this.resourceKey));
}
get script(): string {
@@ -101,6 +103,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
private resourceUseKeyId: string | null;
constructor(
private readonly projectInfoResource: ProjectInfoResource,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly resourceManagerResource: ResourceManagerResource,
state: IResourceSqlDataSourceState,
@@ -269,17 +272,14 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
await this.write();
await this.scheduler.schedule(undefined, async () => {
if (!this.actions || !this.resourceKey || !this.saved || !name?.trim()) {
if (!this.actions || !this.resourceKey || !this.projectId || !this.saved || !name?.trim()) {
return;
}
// TODO: use createResourceOfType instead
if (!name.toLowerCase().endsWith('.sql')) {
name += '.sql';
}
name = this.projectInfoResource.getNameWithExtension(this.projectId, SCRIPTS_TYPE_ID, name);
this.lastAction = this.rename.bind(this, name);
this.message = 'Renaming script...';
this.message = 'plugin_sql_editor_navigation_tab_script_state_renaming';
try {
this.exception = null;
@@ -299,7 +299,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
this.lastAction = this.read.bind(this);
this.message = 'Reading script...';
this.message = 'plugin_sql_editor_navigation_tab_script_state_reading';
try {
this.exception = null;
@@ -334,7 +334,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
this.lastAction = this.write.bind(this);
this.message = 'Saving script...';
this.message = 'plugin_sql_editor_navigation_tab_script_state_saving';
try {
this.exception = null;
@@ -360,7 +360,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
this.lastAction = this.saveProperties.bind(this);
this.message = 'Update info...';
this.message = 'plugin_sql_editor_navigation_tab_script_state_updating';
try {
this.exception = null;
@@ -18,7 +18,8 @@ import { CachedMapAllKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils }
import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
import { throttle } from '@cloudbeaver/core-utils';
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
import { NavResourceNodeService, ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
import { NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm';
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
import { ResourceManagerScriptsService } from '@cloudbeaver/plugin-resource-manager-scripts';
import { getSqlEditorName, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor';
import { SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
@@ -90,7 +91,12 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
this.sqlDataSourceService.register({
key: ResourceSqlDataSource.key,
getDataSource: (editorId, options) => {
const dataSource = new ResourceSqlDataSource(this.connectionInfoResource, this.resourceManagerResource, this.createState(editorId));
const dataSource = new ResourceSqlDataSource(
this.projectInfoResource,
this.connectionInfoResource,
this.resourceManagerResource,
this.createState(editorId),
);
if (options?.executionContext) {
dataSource.setExecutionContext(options.executionContext);
@@ -204,12 +210,15 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
return;
}
const dataSource = this.sqlDataSourceService.dataSources
const dataSources = this.sqlDataSourceService.dataSources
.filter(([, dataSource]) => dataSource instanceof ResourceSqlDataSource)
.map(([, dataSource]) => dataSource as ResourceSqlDataSource)
.find(ds => ds.resourceKey && ds.resourceKey === data.from);
.map(([, dataSource]) => dataSource as ResourceSqlDataSource);
dataSource?.setResourceKey(data.to);
for (const dataSource of dataSources) {
if (dataSource.resourceKey && dataSource.resourceKey.startsWith(data.from)) {
dataSource?.setResourceKey(data.to + dataSource.resourceKey.slice(data.from.length));
}
}
}
private resourceDeleteHandler(keyObj: ResourceKeySimple<string>) {
@@ -219,14 +228,20 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
const tabs: string[] = [];
const dataSources = this.sqlDataSourceService.dataSources
.filter(([, dataSource]) => dataSource instanceof ResourceSqlDataSource)
.map(([, dataSource]) => dataSource as ResourceSqlDataSource);
ResourceKeyUtils.forEach(keyObj, key => {
const tab = this.sqlEditorTabResourceService.getResourceTab(key);
for (const dataSource of dataSources) {
if (dataSource.resourceKey && dataSource.resourceKey.startsWith(key)) {
const tab = this.sqlEditorTabResourceService.getResourceTab(dataSource.resourceKey);
if (tab) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId) as ResourceSqlDataSource;
dataSource?.setResourceKey(undefined); // prevent deleted resource refresh
tabs.push(tab.id);
if (tab) {
tabs.push(tab.id);
}
dataSource.setResourceKey(undefined); // prevent deleted resource refresh
}
}
});
@@ -1,4 +1,8 @@
export default [
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'],
['plugin_sql_editor_navigation_tab_script_state_updating', 'Updating script info...'],
['plugin_sql_editor_navigation_tab_resource_save_script_success', 'Script successfully saved'],
['plugin_sql_editor_navigation_tab_resource_open_script_error', 'Failed to open the script'],
['plugin_sql_editor_navigation_tab_resource_save_script_error', 'Error occurred while trying to save the script'],
@@ -1,4 +1,8 @@
export default [
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'],
['plugin_sql_editor_navigation_tab_script_state_updating', 'Updating script info...'],
['plugin_sql_editor_navigation_tab_resource_save_script_success', 'Script successfully saved'],
['plugin_sql_editor_navigation_tab_resource_open_script_error', 'Failed to open the script'],
['plugin_sql_editor_navigation_tab_resource_save_script_error', 'Error occurred while trying to save the script'],
@@ -1,4 +1,8 @@
export default [
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Переименовывание скрипта...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Чтение скрипта...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Сохранение скрипта...'],
['plugin_sql_editor_navigation_tab_script_state_updating', 'Обновление информации о скрипте...'],
['plugin_sql_editor_navigation_tab_resource_save_script_success', 'Скрипт успешно сохранен'],
['plugin_sql_editor_navigation_tab_resource_open_script_error', 'Не удалось открыть скрипт'],
['plugin_sql_editor_navigation_tab_resource_save_script_error', 'Возникла ошибка при попытки сохранить скрипт'],
@@ -1,4 +1,8 @@
export default [
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'],
['plugin_sql_editor_navigation_tab_script_state_updating', 'Updating script info...'],
['plugin_sql_editor_navigation_tab_resource_save_script_success', '脚本保存成功'],
['plugin_sql_editor_navigation_tab_resource_open_script_error', '打开脚本失败'],
['plugin_sql_editor_navigation_tab_resource_save_script_error', '打开脚本出错'],
@@ -144,7 +144,7 @@ export class SqlEditorBootstrap extends Bootstrap {
const result = await this.commonDialogService.open(RenameDialog, {
value: regexp?.[1] ?? name,
objectName: name,
icon: '/icons/sql_script_m.svg',
icon: dataSource.icon,
validation: name =>
!this.sqlEditorTabService.sqlEditorTabs.some(
tab => tab.handlerState.order !== state.order && this.sqlEditorService.getName(tab.handlerState) === name.trim(),
@@ -42,16 +42,18 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable];
}
protected saved: boolean;
readonly icon: string;
readonly history: ISqlDataSourceHistory;
readonly onUpdate: ISyncExecutor;
readonly onSetScript: ISyncExecutor<ISetScriptData>;
readonly onDatabaseModelUpdate: ISyncExecutor<IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>[]>;
protected saved: boolean;
protected outdated: boolean;
protected editing: boolean;
constructor() {
constructor(icon = '/icons/sql_script_m.svg') {
this.icon = icon;
this.databaseModels = [];
this.exception = undefined;
this.message = undefined;