diff --git a/server/product/web-server/CloudbeaverServer.product.launch b/server/product/web-server/CloudbeaverServer.product.launch
index c7943aa377..f2752415aa 100644
--- a/server/product/web-server/CloudbeaverServer.product.launch
+++ b/server/product/web-server/CloudbeaverServer.product.launch
@@ -88,7 +88,6 @@
-
@@ -97,8 +96,6 @@
-
-
diff --git a/webapp/packages/core-blocks/src/Icon.test.tsx b/webapp/packages/core-blocks/src/Icon.test.tsx
index e8cb556439..ba40ab41d9 100644
--- a/webapp/packages/core-blocks/src/Icon.test.tsx
+++ b/webapp/packages/core-blocks/src/Icon.test.tsx
@@ -15,19 +15,19 @@ test('icons.svg#name', () => {
(globalThis as any)._ROOT_URI_ = undefined;
render();
- 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();
- 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();
- 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');
});
\ No newline at end of file
diff --git a/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx b/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx
index f1be92dfaa..0117356d7c 100644
--- a/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx
+++ b/webapp/packages/core-blocks/src/Snackbars/ActionSnackbar.tsx
@@ -29,7 +29,7 @@ export const ActionSnackbar: NotificationComponent = observ
const translate = useTranslate();
return (
- notification.close(false)}>
+ notification.close(false)}>
diff --git a/webapp/packages/core-blocks/src/Snackbars/ProcessSnackbar.tsx b/webapp/packages/core-blocks/src/Snackbars/ProcessSnackbar.tsx
index 4ae244dd98..8e4419374b 100644
--- a/webapp/packages/core-blocks/src/Snackbars/ProcessSnackbar.tsx
+++ b/webapp/packages/core-blocks/src/Snackbars/ProcessSnackbar.tsx
@@ -45,7 +45,7 @@ export const ProcessSnackbar: NotificationComponent = observer(function P
return (
notification.close(false)}
>
diff --git a/webapp/packages/core-blocks/src/Snackbars/Snackbar.tsx b/webapp/packages/core-blocks/src/Snackbars/Snackbar.tsx
index 07a42af1a1..a58d4833dc 100644
--- a/webapp/packages/core-blocks/src/Snackbars/Snackbar.tsx
+++ b/webapp/packages/core-blocks/src/Snackbars/Snackbar.tsx
@@ -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(function Snackbar({
type,
message,
title,
+ persistent,
closeDelay = 0,
disableShowDetails,
onClose,
@@ -46,7 +48,7 @@ export const Snackbar = observer(function Snackbar({
useStateDelay(closeDelay > 0, closeDelay, onClose);
return (
- onClose(false)}>
+ onClose(false)}>
diff --git a/webapp/packages/core-blocks/src/Snackbars/SnackbarMarkups/SnackbarWrapper.tsx b/webapp/packages/core-blocks/src/Snackbars/SnackbarMarkups/SnackbarWrapper.tsx
index 3db37d09d9..b141023bb3 100644
--- a/webapp/packages/core-blocks/src/Snackbars/SnackbarMarkups/SnackbarWrapper.tsx
+++ b/webapp/packages/core-blocks/src/Snackbars/SnackbarMarkups/SnackbarWrapper.tsx
@@ -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> = 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> = functio
return styled(styles)(
{children}
- {!unclosable && onClose && (
+ {!persistent && onClose && (
)}
diff --git a/webapp/packages/core-cli/configs/webpack.config.js b/webapp/packages/core-cli/configs/webpack.config.js
index 06262a08c8..64992aca71 100644
--- a/webapp/packages/core-cli/configs/webpack.config.js
+++ b/webapp/packages/core-cli/configs/webpack.config.js
@@ -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,
diff --git a/webapp/packages/core-notifications/src/NotificationsItem/NotificationsItem.tsx b/webapp/packages/core-notifications/src/NotificationsItem/NotificationsItem.tsx
index 153f2c494e..c8c773bb80 100644
--- a/webapp/packages/core-notifications/src/NotificationsItem/NotificationsItem.tsx
+++ b/webapp/packages/core-notifications/src/NotificationsItem/NotificationsItem.tsx
@@ -30,6 +30,7 @@ export const NotificationsItem = observer(function Notification({ notific
(this, {
- networkState: observable,
- });
this.networkState = true;
this.networkStateExecutor = new Executor();
+ this.networkStateExecutor.setInitialDataGetter(() => this.state);
+
+ makeObservable(this, {
+ networkState: observable,
+ });
}
register(): void {
diff --git a/webapp/packages/core-utils/src/GlobalConstants.ts b/webapp/packages/core-utils/src/GlobalConstants.ts
index 9aecb2debb..8bb94743f8 100644
--- a/webapp/packages/core-utils/src/GlobalConstants.ts
+++ b/webapp/packages/core-utils/src/GlobalConstants.ts
@@ -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 {
diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts
index 63bbbc6e88..a4ec9351de 100644
--- a/webapp/packages/core-utils/src/index.ts
+++ b/webapp/packages/core-utils/src/index.ts
@@ -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';
diff --git a/webapp/packages/core-utils/src/pathJoin.ts b/webapp/packages/core-utils/src/pathJoin.ts
new file mode 100644
index 0000000000..d5821cbb5e
--- /dev/null
+++ b/webapp/packages/core-utils/src/pathJoin.ts
@@ -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 ? '/' : '.');
+}
\ No newline at end of file
diff --git a/webapp/packages/plugin-codemirror/src/CodeEditor.tsx b/webapp/packages/plugin-codemirror/src/CodeEditor.tsx
index ed5e32b194..4312c1d4ec 100644
--- a/webapp/packages/plugin-codemirror/src/CodeEditor.tsx
+++ b/webapp/packages/plugin-codemirror/src/CodeEditor.tsx
@@ -28,7 +28,7 @@ import { SqlEditorStyles } from './theme';
import { useAutoFormat } from './useAutoFormat';
export const CodeEditor = observer(function CodeEditor(props) {
- const { readonly, autoFormat, className, editorDidMount } = props;
+ const { autoFormat, className, editorDidMount } = props;
const formatter = useAutoFormat(props.options?.mode);
const wrapperRef = useRef(null);
@@ -63,6 +63,8 @@ export const CodeEditor = observer(function CodeEditor(props)
}
}, [editorDidMount]);
+ const readonly = props.options?.readOnly ?? false;
+
return styled(useStyles(SqlEditorStyles))(
(function ExportNotification({
const { title, status, message } = controller.status;
return styled(useStyles(styles))(
-
+
diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx
index cb13757ff8..4c18b84816 100644
--- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx
+++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx
@@ -190,12 +190,12 @@ export const TextValuePresentation: TabContainerPanelComponent;
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;
diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx
index 06335a41b6..d44dbb20ce 100644
--- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx
+++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx
@@ -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))(
- {(expanded || expandedExternal) && (
+ {(expanded || externalExpanded) && (
contextRef.context?.control);
- const Control = control || NavigationNodeControl;
+ const Control = control || externalControl || NavigationNodeControl;
- return (
+ return styled(useStyles(style))(
{/* */}
diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeElement.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeElement.tsx
index a29cf23d9b..750253d841 100644
--- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeElement.tsx
+++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeElement.tsx
@@ -37,6 +37,7 @@ export const NavigationNodeElement: NavTreeNodeComponent = observer(function Nav
return (
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)(
diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx
new file mode 100644
index 0000000000..6c2d95e1f9
--- /dev/null
+++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx
@@ -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(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({
+ rename: () => {
+ setEditing(true);
+ },
+ });
+
+ function handlePortalClick(event: React.MouseEvent) {
+ EventContext.set(event, EventStopPropagationFlag);
+ treeNodeContext.select();
+ }
+
+ function onClickHandler(event: React.MouseEvent) {
+ treeNodeContext.select(event.ctrlKey || event.metaKey);
+ }
+
+ return styled(TREE_NODE_STYLES, styles)(
+
+
+
+ {editing ? (
+ setEditing(false)} />
+ ) : (
+ {node.name}
+ )}
+
+ {!editing && !dndPlaceholder && (
+
+
+
+ )}
+
+ );
+}));
diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx
new file mode 100644
index 0000000000..4b3bf515b1
--- /dev/null
+++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx
@@ -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)(
+
+
+
+ );
+ }
+
+ return (
+
+ );
+});
diff --git a/webapp/packages/plugin-root/src/LocaleService.ts b/webapp/packages/plugin-root/src/LocaleService.ts
new file mode 100644
index 0000000000..90c761184a
--- /dev/null
+++ b/webapp/packages/plugin-root/src/LocaleService.ts
@@ -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 {
+ this.localizationService.addProvider(this.provider.bind(this));
+ }
+
+ load(): void | Promise { }
+
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/webapp/packages/plugin-root/src/NetworkStateNotification/NetworkStateNotificationService.ts b/webapp/packages/plugin-root/src/NetworkStateNotification/NetworkStateNotificationService.ts
new file mode 100644
index 0000000000..5d7a9824d4
--- /dev/null
+++ b/webapp/packages/plugin-root/src/NetworkStateNotification/NetworkStateNotificationService.ts
@@ -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 { }
+
+ 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;
+ }
+ }
+}
diff --git a/webapp/packages/plugin-root/src/locales/en.ts b/webapp/packages/plugin-root/src/locales/en.ts
new file mode 100644
index 0000000000..f5ef171cdf
--- /dev/null
+++ b/webapp/packages/plugin-root/src/locales/en.ts
@@ -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'],
+];
\ No newline at end of file
diff --git a/webapp/packages/plugin-root/src/locales/it.ts b/webapp/packages/plugin-root/src/locales/it.ts
new file mode 100644
index 0000000000..f5ef171cdf
--- /dev/null
+++ b/webapp/packages/plugin-root/src/locales/it.ts
@@ -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'],
+];
\ No newline at end of file
diff --git a/webapp/packages/plugin-root/src/locales/ru.ts b/webapp/packages/plugin-root/src/locales/ru.ts
new file mode 100644
index 0000000000..8056d70e20
--- /dev/null
+++ b/webapp/packages/plugin-root/src/locales/ru.ts
@@ -0,0 +1,4 @@
+export default [
+ ['plugin_root_network_state_title', 'Автономный режим'],
+ ['plugin_root_network_state_description', 'Приложение в автономном режиме, некоторый функционал не доступен'],
+];
\ No newline at end of file
diff --git a/webapp/packages/plugin-root/src/locales/zh.ts b/webapp/packages/plugin-root/src/locales/zh.ts
new file mode 100644
index 0000000000..49a52a5d07
--- /dev/null
+++ b/webapp/packages/plugin-root/src/locales/zh.ts
@@ -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'],
+];
diff --git a/webapp/packages/plugin-root/src/manifest.ts b/webapp/packages/plugin-root/src/manifest.ts
index 4ae168f8bd..3a4d6dab14 100644
--- a/webapp/packages/plugin-root/src/manifest.ts
+++ b/webapp/packages/plugin-root/src/manifest.ts
@@ -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,
],
};
\ No newline at end of file
diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSource.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSource.ts
index e6773084fe..237270e574 100644
--- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSource.ts
+++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSource.ts
@@ -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;
read(dataSource: ResourceSqlDataSource, nodeId: string): Promise;
@@ -53,6 +57,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
private _script: string;
private saved: boolean;
private actions?: IResourceActions;
+ private info?: IResourceInfo;
private lastAction?: () => Promise;
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);
}
diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSourceBootstrap.ts
index e103c06e8f..3715a06766 100644
--- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSourceBootstrap.ts
+++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSourceBootstrap.ts
@@ -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();
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),
diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts
index 1a037571fc..cae0ec0170 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts
@@ -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;
}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts
index 044e4a685a..5568db5333 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts
@@ -20,6 +20,7 @@ export interface ISqlDataSource extends ILoadableState {
readonly onSetScript: ISyncExecutor;
readonly features: ESqlDataSourceFeatures[];
+ isReadonly(): boolean;
isOutdated(): boolean;
markOutdated(): void;
markUpdated(): void;
diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts
index b0f2133196..c7917b2d34 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts
@@ -43,6 +43,10 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource {
});
}
+ isReadonly(): boolean {
+ return false;
+ }
+
setName(name: string | null): void {
this.state.name = name ?? undefined;
}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts
index 585b94b11c..98c6566b15 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts
@@ -52,6 +52,10 @@ export class MemorySqlDataSource extends BaseSqlDataSource {
});
}
+ isReadonly(): boolean {
+ return false;
+ }
+
setScript(script: string): void {
this._script = script;
super.setScript(script);
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLCodeEditor/SQLCodeEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLCodeEditor/SQLCodeEditor.tsx
index 099b7e8a27..98a7fde682 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLCodeEditor/SQLCodeEditor.tsx
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLCodeEditor/SQLCodeEditor.tsx
@@ -30,11 +30,11 @@ export const SQLCodeEditor = observer
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts
index 44286de4d6..19c363af76 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts
@@ -95,6 +95,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
this.executingScript
|| this.readonlyState
|| !!this.dataSource?.isOutdated()
+ || !!this.dataSource?.isReadonly()
);
},
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx
index 87d2d2fdc0..9640f2b9a4 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx
@@ -82,7 +82,6 @@ export function useTools(state: ISqlEditorTabState): Readonly {
this.notificationService.logInfo({
title: 'sql_editor_upload_script_max_size_title',
message: `Max size: ${maxSize}KB\nFile size: ${size}KB`,
- persistent: true,
});
return false;