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;