mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge remote-tracking branch 'origin/devel' into feat/CB-2302/dnd-folders
This commit is contained in:
@@ -88,7 +88,6 @@
|
||||
<setEntry value="org.eclipse.osgi@-1:true"/>
|
||||
<setEntry value="org.eclipse.text@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.apache.dbcp@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.apache.kerby@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.apache.poi@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.cassandra@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.couchbase@default:default"/>
|
||||
@@ -97,8 +96,6 @@
|
||||
<setEntry value="org.jkiss.bundle.graphql.java@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.influxdb@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.jaxb@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.jedis@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.lightcouch@default:default"/>
|
||||
<setEntry value="org.jkiss.bundle.sshj@default:default"/>
|
||||
<setEntry value="org.objectweb.asm*9.1.0.v20210209-1849@default:default"/>
|
||||
<setEntry value="org.slf4j.api@default:default"/>
|
||||
|
||||
@@ -15,19 +15,19 @@ test('icons.svg#name', () => {
|
||||
(globalThis as any)._ROOT_URI_ = undefined;
|
||||
|
||||
render(<Icon name='test' data-testid="icon" />);
|
||||
expect(screen.getByTestId('icon').querySelector('use')).toHaveAttribute('href', '\\icons\\icons.svg#test');
|
||||
expect(screen.getByTestId('icon').querySelector('use')).toHaveAttribute('href', '/icons/icons.svg#test');
|
||||
});
|
||||
|
||||
test('/image.jpg', () => {
|
||||
(globalThis as any)._ROOT_URI_ = undefined;
|
||||
|
||||
render(<Icon name='/image.jpg' data-testid="icon" />);
|
||||
expect(screen.getByTestId('icon').querySelector('use')).toHaveAttribute('href', '\\image.jpg');
|
||||
expect(screen.getByTestId('icon').querySelector('use')).toHaveAttribute('href', '/image.jpg');
|
||||
});
|
||||
|
||||
test('{_ROOT_URI_}/icons.svg#name', () => {
|
||||
(globalThis as any)._ROOT_URI_ = '\\path\\';
|
||||
(globalThis as any)._ROOT_URI_ = '/path/';
|
||||
|
||||
render(<Icon name='test' data-testid="icon" />);
|
||||
expect(screen.getByTestId('icon').querySelector('use')).toHaveAttribute('href', '\\path\\icons\\icons.svg#test');
|
||||
expect(screen.getByTestId('icon').querySelector('use')).toHaveAttribute('href', '/path/icons/icons.svg#test');
|
||||
});
|
||||
@@ -29,7 +29,7 @@ export const ActionSnackbar: NotificationComponent<ActionSnackbarProps> = observ
|
||||
const translate = useTranslate();
|
||||
|
||||
return (
|
||||
<SnackbarWrapper unclosable={notification.persistent} onClose={() => notification.close(false)}>
|
||||
<SnackbarWrapper persistent={notification.persistent} onClose={() => notification.close(false)}>
|
||||
<SnackbarStatus status={notification.type} />
|
||||
<SnackbarContent>
|
||||
<SnackbarBody title={translate(notification.title)}>
|
||||
|
||||
@@ -45,7 +45,7 @@ export const ProcessSnackbar: NotificationComponent<Props> = observer(function P
|
||||
return (
|
||||
<SnackbarWrapper
|
||||
closing={!!notification.state.deleteDelay}
|
||||
unclosable={status === ENotificationType.Loading}
|
||||
persistent={status === ENotificationType.Loading}
|
||||
onClose={() => notification.close(false)}
|
||||
>
|
||||
<SnackbarStatus status={status} />
|
||||
|
||||
@@ -23,6 +23,7 @@ interface SnackbarProps {
|
||||
type: ENotificationType;
|
||||
message?: string;
|
||||
title: string;
|
||||
persistent?: boolean;
|
||||
closeDelay: number;
|
||||
disableShowDetails?: boolean;
|
||||
time: number;
|
||||
@@ -35,6 +36,7 @@ export const Snackbar = observer<SnackbarProps>(function Snackbar({
|
||||
type,
|
||||
message,
|
||||
title,
|
||||
persistent,
|
||||
closeDelay = 0,
|
||||
disableShowDetails,
|
||||
onClose,
|
||||
@@ -46,7 +48,7 @@ export const Snackbar = observer<SnackbarProps>(function Snackbar({
|
||||
useStateDelay(closeDelay > 0, closeDelay, onClose);
|
||||
|
||||
return (
|
||||
<SnackbarWrapper closing={!!state?.deleteDelay} onClose={() => onClose(false)}>
|
||||
<SnackbarWrapper closing={!!state?.deleteDelay} persistent={persistent} onClose={() => onClose(false)}>
|
||||
<SnackbarStatus status={type} />
|
||||
<SnackbarContent>
|
||||
<SnackbarBody title={translate(title)}>
|
||||
|
||||
@@ -51,13 +51,13 @@ const SNACKBAR_WRAPPER_STYLES = css`
|
||||
|
||||
interface Props {
|
||||
closing?: boolean;
|
||||
unclosable?: boolean;
|
||||
persistent?: boolean;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SnackbarWrapper: React.FC<React.PropsWithChildren<Props>> = function SnackbarWrapper({
|
||||
closing = false, unclosable, onClose, children, className,
|
||||
closing = false, persistent, onClose, children, className,
|
||||
}) {
|
||||
const styles = useStyles(SNACKBAR_WRAPPER_STYLES);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -69,7 +69,7 @@ export const SnackbarWrapper: React.FC<React.PropsWithChildren<Props>> = functio
|
||||
return styled(styles)(
|
||||
<notification as="div" className={className} {...use({ mounted, closing })}>
|
||||
{children}
|
||||
{!unclosable && onClose && (
|
||||
{!persistent && onClose && (
|
||||
<IconButton name="cross" viewBox="0 0 16 16" onClick={onClose} />
|
||||
)}
|
||||
</notification>
|
||||
|
||||
@@ -123,7 +123,7 @@ module.exports = (env, argv) => {
|
||||
// "react/jsx-dev-runtime.js": "react/jsx-dev-runtime"
|
||||
// },
|
||||
fallback: {
|
||||
path: require.resolve('path-browserify'),
|
||||
// path: require.resolve('path-browserify'),
|
||||
},
|
||||
plugins: [
|
||||
PnpWebpackPlugin,
|
||||
|
||||
@@ -30,6 +30,7 @@ export const NotificationsItem = observer<Props>(function Notification({ notific
|
||||
<Snackbar
|
||||
title={notification.title}
|
||||
message={notification.message}
|
||||
persistent={notification.persistent}
|
||||
type={notification.type}
|
||||
time={notification.timestamp}
|
||||
state={notification.state}
|
||||
|
||||
@@ -24,15 +24,17 @@ export class NetworkStateService extends Bootstrap {
|
||||
private networkState: boolean;
|
||||
|
||||
constructor(
|
||||
private graphQLService: GraphQLService
|
||||
private readonly graphQLService: GraphQLService
|
||||
) {
|
||||
super();
|
||||
makeObservable<NetworkStateService, 'networkState'>(this, {
|
||||
networkState: observable,
|
||||
});
|
||||
|
||||
this.networkState = true;
|
||||
this.networkStateExecutor = new Executor();
|
||||
this.networkStateExecutor.setInitialDataGetter(() => this.state);
|
||||
|
||||
makeObservable<NetworkStateService, 'networkState'>(this, {
|
||||
networkState: observable,
|
||||
});
|
||||
}
|
||||
|
||||
register(): void {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { pathJoin } from './pathJoin';
|
||||
|
||||
declare const _VERSION_: string | undefined;
|
||||
declare const _DEV_: boolean | undefined;
|
||||
@@ -27,19 +27,19 @@ export const GlobalConstants = {
|
||||
if (_ROOT_URI_ === '{ROOT_URI}') {
|
||||
return defaultURI;
|
||||
}
|
||||
return path.join(_ROOT_URI_ ?? defaultURI, '/');
|
||||
return pathJoin(_ROOT_URI_ ?? defaultURI, '/');
|
||||
},
|
||||
|
||||
get serviceURI(): string {
|
||||
return path.join(this.rootURI, 'api');
|
||||
return pathJoin(this.rootURI, 'api');
|
||||
},
|
||||
|
||||
absoluteRootUrl(...parts: string[]): string {
|
||||
return path.join(this.rootURI, ...parts);
|
||||
return pathJoin(this.rootURI, ...parts);
|
||||
},
|
||||
|
||||
absoluteServiceUrl(...parts: string[]): string {
|
||||
return path.join(this.serviceURI, ...parts);
|
||||
return pathJoin(this.serviceURI, ...parts);
|
||||
},
|
||||
|
||||
absoluteUrl(...parts: string[]): string {
|
||||
|
||||
@@ -23,6 +23,7 @@ export * from './md5';
|
||||
export * from './MetadataMap';
|
||||
export * from './OrderedMap';
|
||||
export * from './parseJSONFlat';
|
||||
export * from './pathJoin';
|
||||
export * from './PriorityQueue';
|
||||
export * from './replaceMiddle';
|
||||
export * from './TextTools';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Joins path segments. Preserves initial "/" and resolves ".." and "."
|
||||
// Does not support using ".." to go above/outside the root.
|
||||
// This means that join("foo", "../../bar") will not resolve to "../bar"
|
||||
export function pathJoin(...segments: string[]): string {
|
||||
// Split the inputs into a list of path commands.
|
||||
let parts: string[] = [];
|
||||
for (const segment of segments) {
|
||||
parts = parts.concat(segment.split('/'));
|
||||
}
|
||||
|
||||
// Interpret the path commands to get the new resolved path.
|
||||
const newParts = [];
|
||||
for (const part of parts) {
|
||||
// Remove leading and trailing slashes
|
||||
// Also remove "." segments
|
||||
if (!part || part === '.') {continue;}
|
||||
// Interpret ".." to pop the last segment
|
||||
if (part === '..') {newParts.pop();}
|
||||
// Push new path segments.
|
||||
else {newParts.push(part);}
|
||||
}
|
||||
// Preserve the initial slash if there was one.
|
||||
if (parts[0] === '') {newParts.unshift('');}
|
||||
// Turn back into a single string path.
|
||||
return newParts.join('/') || (newParts.length ? '/' : '.');
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import { SqlEditorStyles } from './theme';
|
||||
import { useAutoFormat } from './useAutoFormat';
|
||||
|
||||
export const CodeEditor = observer<ICodeEditorProps>(function CodeEditor(props) {
|
||||
const { readonly, autoFormat, className, editorDidMount } = props;
|
||||
const { autoFormat, className, editorDidMount } = props;
|
||||
|
||||
const formatter = useAutoFormat(props.options?.mode);
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -63,6 +63,8 @@ export const CodeEditor = observer<ICodeEditorProps>(function CodeEditor(props)
|
||||
}
|
||||
}, [editorDidMount]);
|
||||
|
||||
const readonly = props.options?.readOnly ?? false;
|
||||
|
||||
return styled(useStyles(SqlEditorStyles))(
|
||||
<code-editor ref={wrapperRef} {...use({ readonly })} className={className}>
|
||||
<CodeMirror
|
||||
|
||||
@@ -9,6 +9,5 @@
|
||||
import type { IControlledCodeMirror } from 'react-codemirror2';
|
||||
|
||||
export interface ICodeEditorProps extends IControlledCodeMirror {
|
||||
readonly?: boolean;
|
||||
autoFormat?: boolean;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const ExportNotification = observer<Props>(function ExportNotification({
|
||||
const { title, status, message } = controller.status;
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<SnackbarWrapper unclosable={status === ENotificationType.Loading} onClose={controller.delete}>
|
||||
<SnackbarWrapper persistent={status === ENotificationType.Loading} onClose={controller.delete}>
|
||||
<SnackbarStatus status={status} />
|
||||
<SnackbarContent>
|
||||
<SnackbarBody title={translate(title)}>
|
||||
|
||||
+1
-1
@@ -190,12 +190,12 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
|
||||
{useCodeEditor ? (
|
||||
<CodeEditorLoader
|
||||
key={readonly ? '1' : '0'}
|
||||
readonly={readonly}
|
||||
value={stringValue}
|
||||
autoFormat={autoFormat}
|
||||
options={{
|
||||
mode: state.currentContentType,
|
||||
theme: 'material',
|
||||
readOnly: readonly ? 'nocursor' : false,
|
||||
lineNumbers: true,
|
||||
indentWithTabs: true,
|
||||
smartIndent: true,
|
||||
|
||||
+4
@@ -9,6 +9,7 @@
|
||||
import type React from 'react';
|
||||
|
||||
import type { NavNode } from '@cloudbeaver/core-navigation-tree';
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
|
||||
export type NavTreeNodeComponent = React.FC<{
|
||||
@@ -33,14 +34,17 @@ export type NavTreeControlComponent = React.FC<NavTreeControlProps>;
|
||||
export type NavigationNodeComponent = React.FC<{
|
||||
node: NavNode;
|
||||
component: NavTreeNodeComponent;
|
||||
control?: NavTreeControlComponent | undefined;
|
||||
path: string[];
|
||||
dragging?: boolean;
|
||||
expanded?: boolean;
|
||||
className?: string;
|
||||
style?: ComponentStyle;
|
||||
}>;
|
||||
|
||||
export type NavigationNodeRendererComponent = React.FC<{
|
||||
nodeId: string;
|
||||
path: string[];
|
||||
component: NavTreeNodeComponent;
|
||||
expanded?: boolean;
|
||||
dragging?: boolean;
|
||||
|
||||
+10
-7
@@ -12,6 +12,7 @@ import styled, { use, css } from 'reshadow';
|
||||
import { getComputed, TreeNode } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { NavNodeManagerService, DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_NAV_NODES } from '@cloudbeaver/core-navigation-tree';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
import { useDNDData } from '@cloudbeaver/core-ui';
|
||||
import { useDataContext } from '@cloudbeaver/core-view';
|
||||
|
||||
@@ -21,7 +22,7 @@ import { NavigationNodeControl } from './NavigationNode/NavigationNodeControl';
|
||||
import { NavigationNodeNested } from './NavigationNode/NavigationNodeNested';
|
||||
import { useNavigationNode } from './useNavigationNode';
|
||||
|
||||
const style = css`
|
||||
const styles = css`
|
||||
TreeNode[|hovered] ::before {
|
||||
opacity: 0.16;
|
||||
}
|
||||
@@ -31,8 +32,10 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
|
||||
node,
|
||||
component,
|
||||
path,
|
||||
expanded: expandedExternal,
|
||||
control: externalControl,
|
||||
expanded: externalExpanded,
|
||||
className,
|
||||
style,
|
||||
}) {
|
||||
const navNodeManagerService = useService(NavNodeManagerService);
|
||||
const {
|
||||
@@ -73,10 +76,10 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
|
||||
context.set(DATA_CONTEXT_NAV_NODE, node);
|
||||
context.set(DATA_CONTEXT_NAV_NODES, getSelected);
|
||||
|
||||
const Control = control || NavigationNodeControl;
|
||||
const Control = control || externalControl || NavigationNodeControl;
|
||||
|
||||
if (leaf || empty) {
|
||||
expandedExternal = false;
|
||||
externalExpanded = false;
|
||||
}
|
||||
|
||||
function setRef(refObj: HTMLDivElement | null) {
|
||||
@@ -87,7 +90,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
|
||||
|
||||
const hasNodes = getComputed(() => !!dndBox.state.context && dndBox.state.canDrop && dndBox.state.isOverCurrent);
|
||||
|
||||
return styled(style)(
|
||||
return styled(useStyles(style, styles))(
|
||||
<TreeNode
|
||||
ref={dndBox.setRef}
|
||||
group={group}
|
||||
@@ -97,7 +100,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
|
||||
indeterminateSelected={indeterminateSelected}
|
||||
expanded={expanded}
|
||||
showInFilter={showInFilter}
|
||||
externalExpanded={expandedExternal}
|
||||
externalExpanded={externalExpanded}
|
||||
leaf={leaf}
|
||||
className={className}
|
||||
onExpand={expand}
|
||||
@@ -112,7 +115,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat
|
||||
dndElement={dndData.state.isDragging}
|
||||
node={node}
|
||||
/>
|
||||
{(expanded || expandedExternal) && (
|
||||
{(expanded || externalExpanded) && (
|
||||
<NavigationNodeNested
|
||||
nodeId={node.id}
|
||||
path={path}
|
||||
|
||||
+6
-2
@@ -8,8 +8,10 @@
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { getComputed, TreeNode, useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { ElementsTreeContext } from '../ElementsTreeContext';
|
||||
import type { NavigationNodeComponent } from '../NavigationNodeComponent';
|
||||
@@ -18,15 +20,17 @@ import { NavigationNodeControl } from './NavigationNode/NavigationNodeControl';
|
||||
export const NavigationNodeDragged: NavigationNodeComponent = observer(function NavigationNode({
|
||||
node,
|
||||
className,
|
||||
control: externalControl,
|
||||
style,
|
||||
}) {
|
||||
const contextRef = useObjectRef({
|
||||
context: useContext(ElementsTreeContext),
|
||||
});
|
||||
const control = getComputed(() => contextRef.context?.control);
|
||||
|
||||
const Control = control || NavigationNodeControl;
|
||||
const Control = control || externalControl || NavigationNodeControl;
|
||||
|
||||
return (
|
||||
return styled(useStyles(style))(
|
||||
<TreeNode externalExpanded={false} className={className} leaf>
|
||||
{/* <DNDPreview data={dndData} src="/icons/empty.svg" /> */}
|
||||
<Control node={node} dndPlaceholder />
|
||||
|
||||
+1
@@ -37,6 +37,7 @@ export const NavigationNodeElement: NavTreeNodeComponent = observer(function Nav
|
||||
return (
|
||||
<CustomRenderer
|
||||
nodeId={nodeId}
|
||||
path={path}
|
||||
expanded={expanded}
|
||||
dragging={dragging}
|
||||
className={className}
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { IElementsTreeSettings } from './ElementsTree/useElementsTree';
|
||||
import { getNavigationTreeUserSettingsId } from './getNavigationTreeUserSettingsId';
|
||||
import { navigationTreeDuplicateFilter } from './navigationTreeDuplicateIdFilter';
|
||||
import { NavigationTreeService } from './NavigationTreeService';
|
||||
import { navigationTreeProjectsRendererRenderer } from './ProjectsRenderer/navigationTreeProjectsRendererRenderer';
|
||||
import { useNavigationTree } from './useNavigationTree';
|
||||
|
||||
const navigationTreeStyles = css`
|
||||
@@ -90,6 +91,10 @@ export const NavigationTree = observer(function NavigationTree() {
|
||||
);
|
||||
|
||||
const duplicateFilter = useMemo(() => navigationTreeDuplicateFilter(navNodeViewService), [navNodeViewService]);
|
||||
const projectsRendererRenderer = useMemo(
|
||||
() => navigationTreeProjectsRendererRenderer(navNodeInfoResource),
|
||||
[navNodeViewService]
|
||||
);
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
@@ -101,7 +106,7 @@ export const NavigationTree = observer(function NavigationTree() {
|
||||
root={root}
|
||||
localState={navTreeService.treeState}
|
||||
filters={[duplicateFilter, connectionGroupFilter]}
|
||||
renderers={[navigationTreeConnectionGroupRenderer]}
|
||||
renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer]}
|
||||
emptyPlaceholder={() => styled(navigationTreeStyles)(
|
||||
<center>
|
||||
<message>
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
import React, { forwardRef, useContext, useState } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
|
||||
import { NavNodeInfoResource, type INodeActions } from '@cloudbeaver/core-navigation-tree';
|
||||
|
||||
import { ElementsTreeContext } from '../ElementsTree/ElementsTreeContext';
|
||||
import type { NavTreeControlComponent, NavTreeControlProps } from '../ElementsTree/NavigationNodeComponent';
|
||||
import { NavigationNodeEditor } from '../ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor';
|
||||
import { TreeNodeMenu } from '../ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu';
|
||||
|
||||
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;
|
||||
}
|
||||
portal {
|
||||
position: relative;
|
||||
}
|
||||
TreeNodeName {
|
||||
composes: theme-text-text-hint-on-light theme-typography--caption from global;
|
||||
height: 100%;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
portal {
|
||||
box-sizing: border-box;
|
||||
margin-left: auto !important;
|
||||
margin-right: 16px !important;
|
||||
visibility: hidden;
|
||||
}
|
||||
name-box {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
|
||||
export const NavigationNodeProjectControl: NavTreeControlComponent = observer<NavTreeControlProps, HTMLDivElement>(forwardRef(function NavigationNodeControl({
|
||||
node,
|
||||
dndElement,
|
||||
dndPlaceholder,
|
||||
}, ref) {
|
||||
const treeNodeContext = useContext(TreeNodeContext);
|
||||
const treeContext = useContext(ElementsTreeContext);
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading);
|
||||
const selected = treeNodeContext.selected;
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
const nodeActions = useObjectRef<INodeActions>({
|
||||
rename: () => {
|
||||
setEditing(true);
|
||||
},
|
||||
});
|
||||
|
||||
function handlePortalClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
EventContext.set(event, EventStopPropagationFlag);
|
||||
treeNodeContext.select();
|
||||
}
|
||||
|
||||
function onClickHandler(event: React.MouseEvent<HTMLDivElement>) {
|
||||
treeNodeContext.select(event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
return styled(TREE_NODE_STYLES, styles)(
|
||||
<TreeNodeControl ref={ref} onClick={onClickHandler} {...use({ outdated, editing, dragging: dndElement })}>
|
||||
<TreeNodeExpand filterActive={treeContext?.tree.filtering} />
|
||||
<TreeNodeName title={node.name}>
|
||||
{editing ? (
|
||||
<NavigationNodeEditor node={node} onClose={() => setEditing(false)} />
|
||||
) : (
|
||||
<name-box>{node.name}</name-box>
|
||||
)}
|
||||
</TreeNodeName>
|
||||
{!editing && !dndPlaceholder && (
|
||||
<portal onClick={handlePortalClick}>
|
||||
<TreeNodeMenu node={node} actions={nodeActions} selected={selected} />
|
||||
</portal>
|
||||
)}
|
||||
</TreeNodeControl>
|
||||
);
|
||||
}));
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { Translate } from '@cloudbeaver/core-localization';
|
||||
import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
|
||||
import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects';
|
||||
|
||||
import { useNode } from '../../NodesManager/useNode';
|
||||
import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent';
|
||||
import { NavigationNodeRenderer } from '../ElementsTree/NavigationTreeNode/NavigationNodeRenderer';
|
||||
import type { IElementsTreeCustomRenderer } from '../ElementsTree/useElementsTree';
|
||||
import { NavigationNodeProjectControl } from './NavigationNodeProjectControl';
|
||||
|
||||
export function navigationTreeProjectsRendererRenderer(
|
||||
navNodeInfoResource: NavNodeInfoResource
|
||||
): IElementsTreeCustomRenderer {
|
||||
|
||||
return nodeId => {
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
|
||||
if (node?.nodeType === NAV_NODE_TYPE_PROJECT) {
|
||||
return ProjectRenderer;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const ProjectRenderer: NavigationNodeRendererComponent = observer(function ManageableGroup({
|
||||
nodeId,
|
||||
path,
|
||||
dragging,
|
||||
component,
|
||||
className,
|
||||
expanded,
|
||||
}) {
|
||||
const { node } = useNode(nodeId);
|
||||
|
||||
if (!node) {
|
||||
return styled(TREE_NODE_STYLES)(
|
||||
<TreeNodeNestedMessage>
|
||||
<Translate token='app_navigationTree_node_not_found' />
|
||||
</TreeNodeNestedMessage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationNodeRenderer
|
||||
node={node}
|
||||
path={path}
|
||||
expanded={expanded}
|
||||
dragging={dragging}
|
||||
className={className}
|
||||
control={NavigationNodeProjectControl}
|
||||
component={component}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
|
||||
@injectable()
|
||||
export class LocaleService extends Bootstrap {
|
||||
constructor(private readonly localizationService: LocalizationService) {
|
||||
super();
|
||||
}
|
||||
|
||||
register(): void | Promise<void> {
|
||||
this.localizationService.addProvider(this.provider.bind(this));
|
||||
}
|
||||
|
||||
load(): void | Promise<void> { }
|
||||
|
||||
private async provider(locale: string) {
|
||||
switch (locale) {
|
||||
case 'ru':
|
||||
return (await import('./locales/ru')).default;
|
||||
case 'it':
|
||||
return (await import('./locales/it')).default;
|
||||
case 'zh':
|
||||
return (await import('./locales/zh')).default;
|
||||
default:
|
||||
return (await import('./locales/en')).default;
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { INotification, NotificationService } from '@cloudbeaver/core-events';
|
||||
import { NetworkStateService } from '@cloudbeaver/core-root';
|
||||
|
||||
|
||||
@injectable()
|
||||
export class NetworkStateNotificationService extends Bootstrap {
|
||||
private activeNotification: INotification | null;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly networkStateService: NetworkStateService
|
||||
) {
|
||||
super();
|
||||
this.activeNotification = null;
|
||||
}
|
||||
|
||||
register(): void {
|
||||
this.networkStateService.networkStateExecutor.addHandler(this.handleNetworkStateChange.bind(this));
|
||||
}
|
||||
|
||||
load(): void | Promise<void> { }
|
||||
|
||||
private handleNetworkStateChange(state: boolean): void {
|
||||
if (!state) {
|
||||
if (this.activeNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeNotification = this.notificationService.logInfo({
|
||||
title: 'plugin_root_network_state_title',
|
||||
message: 'plugin_root_network_state_description',
|
||||
persistent: true,
|
||||
});
|
||||
} else {
|
||||
this.activeNotification?.close(true);
|
||||
this.activeNotification = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default [
|
||||
['plugin_root_network_state_title', 'Offline mode'],
|
||||
['plugin_root_network_state_description', 'Application now in offline mode, some features are disabled'],
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
export default [
|
||||
['plugin_root_network_state_title', 'Offline mode'],
|
||||
['plugin_root_network_state_description', 'Application now in offline mode, some features are disabled'],
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
export default [
|
||||
['plugin_root_network_state_title', 'Автономный режим'],
|
||||
['plugin_root_network_state_description', 'Приложение в автономном режиме, некоторый функционал не доступен'],
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
export default [
|
||||
['plugin_root_network_state_title', 'Offline mode'],
|
||||
['plugin_root_network_state_description', 'Application now in offline mode, some features are disabled'],
|
||||
];
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import type { PluginManifest } from '@cloudbeaver/core-di';
|
||||
|
||||
import { LocaleService } from './LocaleService';
|
||||
import { NetworkStateNotificationService } from './NetworkStateNotification/NetworkStateNotificationService';
|
||||
import { PluginBootstrap } from './PluginBootstrap';
|
||||
import { SessionExpiredDialogService } from './SessionExpireDialog/SessionExpiredDialogService';
|
||||
import { SessionExpireWarningDialogService } from './SessionExpireWarningDialog/SessionExpireWarningDialogService';
|
||||
@@ -15,8 +17,10 @@ import { SessionExpireWarningDialogService } from './SessionExpireWarningDialog/
|
||||
export const rootPlugin: PluginManifest = {
|
||||
info: { name: 'Root plugin' },
|
||||
providers: [
|
||||
LocaleService,
|
||||
PluginBootstrap,
|
||||
SessionExpiredDialogService,
|
||||
SessionExpireWarningDialogService,
|
||||
NetworkStateNotificationService,
|
||||
],
|
||||
};
|
||||
+13
@@ -15,6 +15,10 @@ import { BaseSqlDataSource, ESqlDataSourceFeatures } from '@cloudbeaver/plugin-s
|
||||
|
||||
import type { IResourceNodeInfo, IResourceSqlDataSourceState } from './IResourceSqlDataSourceState';
|
||||
|
||||
interface IResourceInfo {
|
||||
isReadonly?: (dataSource: ResourceSqlDataSource) => boolean;
|
||||
}
|
||||
|
||||
interface IResourceActions {
|
||||
rename(dataSource: ResourceSqlDataSource, nodeId: string, name: string): Promise<string>;
|
||||
read(dataSource: ResourceSqlDataSource, nodeId: string): Promise<string>;
|
||||
@@ -53,6 +57,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
private _script: string;
|
||||
private saved: boolean;
|
||||
private actions?: IResourceActions;
|
||||
private info?: IResourceInfo;
|
||||
private lastAction?: () => Promise<void>;
|
||||
private readonly state: IResourceSqlDataSourceState;
|
||||
|
||||
@@ -85,6 +90,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
isReadonly(): boolean {
|
||||
return !this.isLoaded() || this.info?.isReadonly?.(this) === true;
|
||||
}
|
||||
|
||||
isOutdated(): boolean {
|
||||
return this.nodeInfo !== undefined && super.isOutdated();
|
||||
}
|
||||
@@ -108,6 +117,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
setInfo(info?: IResourceInfo): void {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
setName(name: string | null): void {
|
||||
this.rename(name);
|
||||
}
|
||||
|
||||
+6
-1
@@ -13,7 +13,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { NavTreeResource, NavNodeInfoResource, INavNodeMoveData, INavNodeRenameData } from '@cloudbeaver/core-navigation-tree';
|
||||
import { WindowEventsService } from '@cloudbeaver/core-root';
|
||||
import { NetworkStateService, WindowEventsService } from '@cloudbeaver/core-root';
|
||||
import { ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
|
||||
import { throttle } from '@cloudbeaver/core-utils';
|
||||
@@ -34,6 +34,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
private readonly dataSourceStateState = new Map<string, IResourceSqlDataSourceState>();
|
||||
|
||||
constructor(
|
||||
private readonly networkStateService: NetworkStateService,
|
||||
private readonly sqlDataSourceService: SqlDataSourceService,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
private readonly navResourceNodeService: NavResourceNodeService,
|
||||
@@ -111,6 +112,10 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
write: this.write.bind(this),
|
||||
});
|
||||
|
||||
dataSource.setInfo({
|
||||
isReadonly: () => !this.networkStateService.state,
|
||||
});
|
||||
|
||||
return dataSource;
|
||||
},
|
||||
onDestroy: (_, editorId) => this.deleteState(editorId),
|
||||
|
||||
@@ -46,6 +46,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
|
||||
abstract setName(name: string | null): void;
|
||||
abstract setExecutionContext(executionContext?: IConnectionExecutionContextInfo | undefined): void;
|
||||
|
||||
isReadonly(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isOutdated(): boolean {
|
||||
return this.outdated;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ISqlDataSource extends ILoadableState {
|
||||
readonly onSetScript: ISyncExecutor<string>;
|
||||
readonly features: ESqlDataSourceFeatures[];
|
||||
|
||||
isReadonly(): boolean;
|
||||
isOutdated(): boolean;
|
||||
markOutdated(): void;
|
||||
markUpdated(): void;
|
||||
|
||||
+4
@@ -43,6 +43,10 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
isReadonly(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
setName(name: string | null): void {
|
||||
this.state.name = name ?? undefined;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ export class MemorySqlDataSource extends BaseSqlDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
isReadonly(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
setScript(script: string): void {
|
||||
this._script = script;
|
||||
super.setScript(script);
|
||||
|
||||
@@ -30,11 +30,11 @@ export const SQLCodeEditor = observer<ISQLCodeEditorProps, SQLCodeEditorControll
|
||||
<CodeEditorLoader
|
||||
{...controller.bindings}
|
||||
options={{
|
||||
readOnly: props.readonly ? 'nocursor' : false,
|
||||
...controller.bindings.options,
|
||||
mode: controller.mode,
|
||||
}}
|
||||
className={props.className}
|
||||
readonly={props.readonly}
|
||||
autoFormat={props.autoFormat}
|
||||
value={props.value || ''}
|
||||
/>
|
||||
|
||||
@@ -95,6 +95,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
|
||||
this.executingScript
|
||||
|| this.readonlyState
|
||||
|| !!this.dataSource?.isOutdated()
|
||||
|| !!this.dataSource?.isReadonly()
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ export function useTools(state: ISqlEditorTabState): Readonly<State> {
|
||||
this.notificationService.logInfo({
|
||||
title: 'sql_editor_upload_script_max_size_title',
|
||||
message: `Max size: ${maxSize}KB\nFile size: ${size}KB`,
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user