Merge remote-tracking branch 'origin/devel' into feat/CB-2302/dnd-folders

This commit is contained in:
Wroud
2022-08-25 13:47:31 +03:00
39 changed files with 401 additions and 40 deletions
@@ -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 {
+1
View File
@@ -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)}>
@@ -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,
@@ -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;
@@ -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}
@@ -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 />
@@ -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>
@@ -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>
);
}));
@@ -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;
}
}
}
@@ -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,
],
};
@@ -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);
}
@@ -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;
@@ -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;