mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge remote-tracking branch 'origin' into feature/CB-1594
This commit is contained in:
@@ -21,6 +21,15 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io
|
||||
[Database access instructions](https://github.com/dbeaver/cloudbeaver/wiki/Demo-Server)
|
||||
|
||||
## Changelog
|
||||
### CloudBeaver 21.3.4 - 2022-02-07
|
||||
- The Database Navigator menu allows you to:
|
||||
- filter objects in the Database Navigator,
|
||||
- set the Database Navigator view for every user separately.
|
||||
- Parsing of scripts with delimiters has been improved in the SQL Editor.
|
||||
- The dialog with available shortcuts can be opened in the top menu.
|
||||
- The specific IP address can be set for the CloudBeaver instance in configuration files.
|
||||
- Different small bugs have been fixed.
|
||||
|
||||
### CloudBeaver 21.3.3 - 2022-01-25
|
||||
- SSH public key support is added.
|
||||
- New tab's context menu allows users to close all opened tabs or a group of tabs.
|
||||
|
||||
@@ -133,6 +133,7 @@ type ServerConfig {
|
||||
|
||||
configurationMode: Boolean
|
||||
developmentMode: Boolean
|
||||
redirectOnFederatedAuth: Boolean
|
||||
|
||||
enabledFeatures: [ID!]!
|
||||
enabledAuthProviders: [ID!]!
|
||||
|
||||
@@ -130,6 +130,11 @@ public class WebServerConfig {
|
||||
return application.isDevelMode();
|
||||
}
|
||||
|
||||
@Property
|
||||
public boolean isRedirectOnFederatedAuth() {
|
||||
return application.getAppConfiguration().isRedirectOnFederatedAuth();
|
||||
}
|
||||
|
||||
@Property
|
||||
public long getSessionExpireTime() {
|
||||
return application.getMaxSessionIdleTime();
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.graphql.GraphQLEndpoint;
|
||||
import io.cloudbeaver.server.servlets.CBImageServlet;
|
||||
import io.cloudbeaver.server.servlets.CBStaticServlet;
|
||||
import io.cloudbeaver.server.servlets.CBStatusServlet;
|
||||
import io.cloudbeaver.service.DBWServiceBindingServlet;
|
||||
import org.eclipse.jetty.server.*;
|
||||
import org.eclipse.jetty.server.session.DefaultSessionCache;
|
||||
@@ -60,7 +61,7 @@ public class CBJettyServer {
|
||||
String rootURI = application.getRootURI();
|
||||
servletContextHandler.setContextPath(rootURI);
|
||||
servletContextHandler.addServlet(new ServletHolder("static", new CBStaticServlet()), "/*");
|
||||
servletContextHandler.addServlet(new ServletHolder("status", new CBStaticServlet()), "/status");
|
||||
servletContextHandler.addServlet(new ServletHolder("status", new CBStatusServlet()), "/status");
|
||||
servletContextHandler.addServlet(new ServletHolder("images", new CBImageServlet()), application.getServicesURI() + "images/*");
|
||||
servletContextHandler.addServlet(new ServletHolder("graphql", new GraphQLEndpoint()), application.getServicesURI() + "gql/*");
|
||||
servletContextHandler.addEventListener(new CBServerContextListener());
|
||||
|
||||
+2
-1
@@ -8,4 +8,5 @@ lib
|
||||
coverage
|
||||
ts-out
|
||||
npm-debug.log
|
||||
debug.log
|
||||
debug.log
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -26,6 +26,8 @@ import type { NavTreeControlComponent } from './NavigationNodeComponent';
|
||||
import { NavigationNodeNested } from './NavigationTreeNode/NavigationNode/NavigationNodeNested';
|
||||
import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement';
|
||||
import { NavigationTreeService } from './NavigationTreeService';
|
||||
import { elementsTreeLimitFilter } from './NavTreeLimitFilter/elementsTreeLimitFilter';
|
||||
import { elementsTreeLimitRenderer } from './NavTreeLimitFilter/elementsTreeLimitRenderer';
|
||||
import { ITreeContext, TreeContext } from './TreeContext';
|
||||
import { IElementsTreeCustomRenderer, IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree';
|
||||
|
||||
@@ -65,11 +67,13 @@ const styles = composes(
|
||||
|
||||
interface Props {
|
||||
root?: string;
|
||||
limit?: number;
|
||||
keepData?: boolean;
|
||||
disabled?: boolean;
|
||||
selectionTree?: boolean;
|
||||
foldersTree?: boolean;
|
||||
filter?: boolean;
|
||||
filterAll?: boolean;
|
||||
showFolderExplorerPath?: boolean;
|
||||
localState?: MetadataMap<string, ITreeNodeState>;
|
||||
control?: NavTreeControlComponent;
|
||||
@@ -91,6 +95,7 @@ interface Props {
|
||||
|
||||
export const ElementsTree = observer<Props>(function ElementsTree({
|
||||
root: baseRoot = ROOT_NODE_PATH,
|
||||
limit,
|
||||
control,
|
||||
keepData = false,
|
||||
disabled,
|
||||
@@ -99,10 +104,11 @@ export const ElementsTree = observer<Props>(function ElementsTree({
|
||||
showFolderExplorerPath = false,
|
||||
foldersTree = false,
|
||||
filter = false,
|
||||
filterAll = false,
|
||||
emptyPlaceholder,
|
||||
navNodeFilterCompare,
|
||||
filters,
|
||||
renderers,
|
||||
filters = [],
|
||||
renderers = [],
|
||||
style,
|
||||
className,
|
||||
isGroup,
|
||||
@@ -165,6 +171,11 @@ export const ElementsTree = observer<Props>(function ElementsTree({
|
||||
},
|
||||
});
|
||||
|
||||
const limitFilter = useMemo(() => elementsTreeLimitFilter(
|
||||
navTreeResource,
|
||||
limit
|
||||
), [navTreeResource, limit]);
|
||||
|
||||
const nameFilter = useMemo(() => elementsTreeNameFilter(
|
||||
navTreeResource,
|
||||
navNodeInfoResource,
|
||||
@@ -178,10 +189,11 @@ export const ElementsTree = observer<Props>(function ElementsTree({
|
||||
showFolderExplorerPath,
|
||||
root,
|
||||
disabled,
|
||||
filterAll,
|
||||
keepData,
|
||||
localState,
|
||||
filters: [nameFilter, ...(filters || [])],
|
||||
renderers,
|
||||
filters: [nameFilter, ...filters, limitFilter],
|
||||
renderers: [...renderers, elementsTreeLimitRenderer],
|
||||
isGroup,
|
||||
onFilter,
|
||||
beforeSelect,
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 type { NavNode } from '../../shared/NodesManager/EntityTypes';
|
||||
import type { NavTreeResource } from '../../shared/NodesManager/NavTreeResource';
|
||||
import type { IElementsTreeFilter } from '../useElementsTree';
|
||||
|
||||
export type NavNodeFilterCompareFn = (node: NavNode, filter: string)=> boolean;
|
||||
|
||||
export const NAVIGATION_TREE_LIMIT = {
|
||||
limit: 'nav-tree://limit',
|
||||
};
|
||||
|
||||
export function elementsTreeLimitFilter(
|
||||
navTreeResource: NavTreeResource,
|
||||
limit?: number
|
||||
): IElementsTreeFilter {
|
||||
return (
|
||||
filter: string,
|
||||
node: NavNode,
|
||||
children: string[],
|
||||
) => {
|
||||
limit = limit ?? navTreeResource.childrenLimit;
|
||||
const nextChildren = children.slice(0, limit);
|
||||
|
||||
if (children.length > limit) {
|
||||
nextChildren.push(NAVIGATION_TREE_LIMIT.limit);
|
||||
}
|
||||
|
||||
return nextChildren;
|
||||
};
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 styled, { css } from 'reshadow';
|
||||
|
||||
import { Translate } from '@cloudbeaver/core-localization';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { NavigationNodeRendererComponent } from '../NavigationNodeComponent';
|
||||
import { NAVIGATION_TREE_LIMIT } from './elementsTreeLimitFilter';
|
||||
|
||||
export function elementsTreeLimitRenderer(nodeId: string): NavigationNodeRendererComponent | undefined {
|
||||
if (nodeId === NAVIGATION_TREE_LIMIT.limit) {
|
||||
return ManageableGroup;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
connection-group {
|
||||
composes: theme-text-text-hint-on-light from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
connection-group {
|
||||
composes: theme-typography--caption from global;
|
||||
padding: 4px 32px;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const ManageableGroup: NavigationNodeRendererComponent = function ManageableGroup() {
|
||||
return styled(useStyles(styles))(<connection-group><Translate token='app_navigationTree_limited' /></connection-group>);
|
||||
};
|
||||
@@ -39,7 +39,6 @@ const navigationTreeStyles = css`
|
||||
}
|
||||
|
||||
ElementsTree {
|
||||
padding-top: 8px;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
}
|
||||
@@ -110,6 +109,7 @@ export const NavigationTree = observer(function NavigationTree() {
|
||||
foldersTree={settings.folders}
|
||||
showFolderExplorerPath={settings.folders}
|
||||
filter={settings.filter}
|
||||
filterAll={settings.filterAll}
|
||||
keepData={settings.saveExpanded}
|
||||
onOpen={handleOpen}
|
||||
/>
|
||||
|
||||
@@ -38,10 +38,9 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod
|
||||
});
|
||||
const { isLoading } = useNode(node.id);
|
||||
const children = useChildren(node.id);
|
||||
const state = contextRef.context?.tree.getNodeState(node.id);
|
||||
|
||||
const loading = getComputed(() => isLoading() || children.isLoading());
|
||||
const isExpanded = getComputed(() => state?.expanded || false);
|
||||
const isExpanded = getComputed(() => contextRef.context?.tree.isNodeExpanded(node.id) || false);
|
||||
const leaf = getComputed(() => isLeaf(node));
|
||||
const group = getComputed(() => contextRef.context?.tree.isGroup?.(node) || false);
|
||||
const empty = getComputed(() => children.children?.length === 0);
|
||||
|
||||
@@ -60,7 +60,7 @@ export class NavigationTreeService extends View<string> {
|
||||
}
|
||||
|
||||
getChildren(id: string): string[] | undefined {
|
||||
return this.navTreeResource.get(id);
|
||||
return this.navNodeManagerService.getTree(id);
|
||||
}
|
||||
|
||||
async navToNode(id: string, parentId: string): Promise<void> {
|
||||
|
||||
+1
@@ -8,6 +8,7 @@
|
||||
|
||||
export interface INavigationTreeUserSettings {
|
||||
filter: boolean;
|
||||
filterAll: boolean;
|
||||
saveExpanded: boolean;
|
||||
folders: boolean;
|
||||
}
|
||||
+14
@@ -29,6 +29,10 @@ const expandStyles = css`
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
Group {
|
||||
min-width: 350px;
|
||||
width: min-content;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
@@ -69,6 +73,16 @@ export const NavigationTreeSettings = observer<Props>(function NavigationTreeSet
|
||||
>
|
||||
{translate('app_navigationTree_settings_filter_title')}
|
||||
</Switch>
|
||||
<Switch
|
||||
name="filterAll"
|
||||
state={settings}
|
||||
disabled={!settings.filter}
|
||||
description={translate('app_navigationTree_settings_filter_all_description')}
|
||||
mod={['primary']}
|
||||
small
|
||||
>
|
||||
{translate('app_navigationTree_settings_filter_all_title')}
|
||||
</Switch>
|
||||
<Switch
|
||||
name="saveExpanded"
|
||||
state={settings}
|
||||
|
||||
+2
@@ -13,6 +13,7 @@ import type { INavigationTreeUserSettings } from './INavigationTreeUserSettings'
|
||||
export function createNavigationTreeUserSettings(): INavigationTreeUserSettings {
|
||||
return observable<INavigationTreeUserSettings>({
|
||||
filter: false,
|
||||
filterAll: false,
|
||||
saveExpanded: true,
|
||||
folders: false,
|
||||
});
|
||||
@@ -21,6 +22,7 @@ export function createNavigationTreeUserSettings(): INavigationTreeUserSettings
|
||||
export function validateNavigationTreeUserSettings(data: any): boolean {
|
||||
return (
|
||||
typeof data === 'object'
|
||||
&& typeof data.filterAll === 'boolean'
|
||||
&& typeof data.filter === 'boolean'
|
||||
&& typeof data.saveExpanded === 'boolean'
|
||||
&& typeof data.folders === 'boolean'
|
||||
|
||||
@@ -39,7 +39,7 @@ export function elementsTreeNameFilter(
|
||||
compare,
|
||||
filter,
|
||||
child,
|
||||
state
|
||||
// state
|
||||
));
|
||||
|
||||
return nodes.map(node => node.id);
|
||||
@@ -52,32 +52,32 @@ function filterNode(
|
||||
compare: NavNodeFilterCompareFn,
|
||||
filter: string,
|
||||
node: NavNode,
|
||||
state: MetadataMap<string, ITreeNodeState>
|
||||
// state: MetadataMap<string, ITreeNodeState>
|
||||
): boolean {
|
||||
const nodeState = state.get(node.id);
|
||||
// const nodeState = state.get(node.id);
|
||||
|
||||
|
||||
if (compare(node, filter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nodeState.expanded) {
|
||||
const children = navTreeResource.get(node.id) || [];
|
||||
// if (nodeState.expanded) {
|
||||
const children = navTreeResource.get(node.id) || [];
|
||||
|
||||
return navNodeInfoResource
|
||||
.get(resourceKeyList(children))
|
||||
.filter(isDefined)
|
||||
.some(child => filterNode(
|
||||
navTreeResource,
|
||||
navNodeInfoResource,
|
||||
compare,
|
||||
filter,
|
||||
child,
|
||||
state
|
||||
));
|
||||
}
|
||||
return navNodeInfoResource
|
||||
.get(resourceKeyList(children))
|
||||
.filter(isDefined)
|
||||
.some(child => filterNode(
|
||||
navTreeResource,
|
||||
navNodeInfoResource,
|
||||
compare,
|
||||
filter,
|
||||
child,
|
||||
// state
|
||||
));
|
||||
// }
|
||||
|
||||
return false;
|
||||
// return false;
|
||||
}
|
||||
|
||||
function compareNodes(node: NavNode, filter: string): boolean {
|
||||
|
||||
@@ -47,6 +47,7 @@ interface IOptions {
|
||||
showFolderExplorerPath: boolean;
|
||||
disabled?: boolean;
|
||||
keepData?: boolean;
|
||||
filterAll?: boolean;
|
||||
localState?: MetadataMap<string, ITreeNodeState>;
|
||||
filters?: IElementsTreeFilter[];
|
||||
renderers?: IElementsTreeCustomRenderer[];
|
||||
@@ -64,11 +65,13 @@ export interface IElementsTree {
|
||||
filter: string;
|
||||
loading: boolean;
|
||||
disabled: boolean;
|
||||
filterAll: boolean;
|
||||
foldersTree: boolean;
|
||||
showFolderExplorerPath: boolean;
|
||||
renderers: IElementsTreeCustomRenderer[];
|
||||
state: MetadataMap<string, ITreeNodeState>;
|
||||
getNodeState: (nodeId: string) => ITreeNodeState;
|
||||
isNodeExpanded: (nodeId: string) => boolean;
|
||||
isNodeSelected: (nodeId: string) => boolean;
|
||||
getNodeChildren: (nodeId: string) => string[];
|
||||
isGroup?: (node: NavNode) => boolean;
|
||||
@@ -131,8 +134,8 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
await navNodeInfoResource.waitLoad();
|
||||
await navTreeResource.waitLoad();
|
||||
|
||||
const nodeState = state.get(child);
|
||||
if (!nodeState.expanded && child !== options.root) {
|
||||
const expanded = elementsTree.isNodeExpanded(child);
|
||||
if (!expanded && child !== options.root) {
|
||||
if (navNodeInfoResource.isOutdated(child)) {
|
||||
const node = navNodeInfoResource.get(child);
|
||||
|
||||
@@ -161,28 +164,14 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeChildren(nodeId: string): string[] {
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
|
||||
if (!node) {
|
||||
return []; // Maybe filter should accept nodeId, so we be able to apply filters to empty node
|
||||
}
|
||||
|
||||
return (options.filters || [])
|
||||
.reduce(
|
||||
(children, filter) => filter(elementsTree.filter, node, children, state),
|
||||
navTreeService.getChildren(node.id) || []
|
||||
);
|
||||
}
|
||||
|
||||
function getNestedChildren(nodeId: string): string [] {
|
||||
const nestedChildren: string[] = [];
|
||||
const prevChildren = getNodeChildren(nodeId);
|
||||
const prevChildren = elementsTree.getNodeChildren(nodeId);
|
||||
nestedChildren.push(...prevChildren);
|
||||
|
||||
while (prevChildren.length) {
|
||||
const nodeKey = prevChildren.shift()!;
|
||||
const children = getNodeChildren(nodeKey);
|
||||
const children = elementsTree.getNodeChildren(nodeKey);
|
||||
prevChildren.push(...children);
|
||||
nestedChildren.push(...children);
|
||||
}
|
||||
@@ -227,7 +216,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
}
|
||||
|
||||
if (options.isGroup?.(node)) {
|
||||
const children = getNodeChildren(nodeId);
|
||||
const children = elementsTree.getNodeChildren(nodeId);
|
||||
|
||||
for (const child of children) {
|
||||
await setSelection(child, selected);
|
||||
@@ -252,11 +241,22 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
getNodeState(nodeId: string) {
|
||||
return this.state.get(nodeId);
|
||||
},
|
||||
isNodeExpanded(nodeId: string): boolean {
|
||||
if (nodeId === this.root) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.filter !== '' && this.filterAll) {
|
||||
return this.getNodeChildren(nodeId).length > 0;
|
||||
}
|
||||
|
||||
return this.getNodeState(nodeId).expanded;
|
||||
},
|
||||
isNodeSelected(nodeId: string): boolean {
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
|
||||
if (node && elementsTree.isGroup?.(node)) {
|
||||
const children = getNodeChildren(nodeId);
|
||||
const children = this.getNodeChildren(nodeId);
|
||||
|
||||
if (children.length > 0) {
|
||||
return children.every(child => this.isNodeSelected(child));
|
||||
@@ -267,7 +267,19 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
|
||||
return this.getNodeState(nodeId).selected;
|
||||
},
|
||||
getNodeChildren,
|
||||
getNodeChildren(nodeId: string): string[] {
|
||||
const node = navNodeInfoResource.get(nodeId);
|
||||
|
||||
if (!node) {
|
||||
return []; // Maybe filter should accept nodeId, so we be able to apply filters to empty node
|
||||
}
|
||||
|
||||
return (options.filters || [])
|
||||
.reduce(
|
||||
(children, filter) => filter(elementsTree.filter, node, children, state),
|
||||
navTreeService.getChildren(node.id) || []
|
||||
);
|
||||
},
|
||||
async setFilter(value: string) {
|
||||
this.filter = value;
|
||||
|
||||
@@ -320,6 +332,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
disabled: observable.ref,
|
||||
root: observable.ref,
|
||||
filter: observable.ref,
|
||||
filterAll: observable.ref,
|
||||
loading: observable.ref,
|
||||
renderers: observable.ref,
|
||||
baseRoot: observable.ref,
|
||||
@@ -328,6 +341,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
disabled: options.disabled,
|
||||
root: options.root,
|
||||
foldersTree,
|
||||
filterAll: options.filterAll,
|
||||
showFolderExplorerPath,
|
||||
baseRoot: options.baseRoot,
|
||||
renderers,
|
||||
|
||||
@@ -12,9 +12,12 @@ export default [
|
||||
['app_topnavbar_connection_schema_manager_not_selected', '-- not selected --'],
|
||||
['app_navigationTree_openNodeTab', 'Open'],
|
||||
['app_navigationTree_search', 'Search elements...'],
|
||||
['app_navigationTree_limited', 'Some elements were hidden'],
|
||||
['app_navigationTree_settings_title', 'Settings'],
|
||||
['app_navigationTree_settings_filter_title', 'Filter'],
|
||||
['app_navigationTree_settings_filter_description', 'Show filtering field'],
|
||||
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
|
||||
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
|
||||
['app_navigationTree_settings_state_title', 'Save state'],
|
||||
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
|
||||
['app_navigationTree_settings_folders_title', 'Folders'],
|
||||
|
||||
@@ -12,9 +12,12 @@ export default [
|
||||
['app_topnavbar_connection_schema_manager_not_selected', '-- non selezionato --'],
|
||||
['app_navigationTree_openNodeTab', 'Apri'],
|
||||
['app_navigationTree_search', 'Search elements...'],
|
||||
['app_navigationTree_limited', 'Some elements were hidden'],
|
||||
['app_navigationTree_settings_title', 'Settings'],
|
||||
['app_navigationTree_settings_filter_title', 'Filter'],
|
||||
['app_navigationTree_settings_filter_description', 'Show filtering field'],
|
||||
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
|
||||
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
|
||||
['app_navigationTree_settings_state_title', 'Save state'],
|
||||
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
|
||||
['app_navigationTree_settings_folders_title', 'Folders'],
|
||||
|
||||
@@ -15,10 +15,13 @@ export default [
|
||||
],
|
||||
['app_navigationTree_openNodeTab', 'Открыть'],
|
||||
['app_navigationTree_search', 'Искать элементы...'],
|
||||
['app_navigationTree_limited', 'Некоторые элементы были скрыты'],
|
||||
|
||||
['app_navigationTree_settings_title', 'Настройки'],
|
||||
['app_navigationTree_settings_filter_title', 'Фильтр'],
|
||||
['app_navigationTree_settings_filter_description', 'Показывать фильтр'],
|
||||
['app_navigationTree_settings_filter_all_title', 'Фильтровать скрытые элементы'],
|
||||
['app_navigationTree_settings_filter_all_description', 'Показывает скрытые элементы при использовании фильтра'],
|
||||
['app_navigationTree_settings_state_title', 'Сохранять состояние'],
|
||||
['app_navigationTree_settings_state_description', 'Восстанавливает открытые узлы после обновления страницы'],
|
||||
['app_navigationTree_settings_folders_title', 'Папки'],
|
||||
|
||||
@@ -11,9 +11,12 @@ export default [
|
||||
['app_shared_settingsMenu_lang', '语言'],
|
||||
['app_topnavbar_connection_schema_manager_not_selected', '-- 为选择 --'],
|
||||
['app_navigationTree_search', 'Search elements...'],
|
||||
['app_navigationTree_limited', 'Some elements were hidden'],
|
||||
['app_navigationTree_settings_title', 'Settings'],
|
||||
['app_navigationTree_settings_filter_title', 'Filter'],
|
||||
['app_navigationTree_settings_filter_description', 'Show filtering field'],
|
||||
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
|
||||
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
|
||||
['app_navigationTree_settings_state_title', 'Save state'],
|
||||
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
|
||||
['app_navigationTree_settings_folders_title', 'Folders'],
|
||||
|
||||
@@ -103,6 +103,7 @@ export class NavNodeContextMenuService extends Bootstrap {
|
||||
subTitle: name,
|
||||
objectName: node.nodeType || 'Object',
|
||||
icon: node.icon,
|
||||
validation: name => name.trim().length > 0,
|
||||
});
|
||||
|
||||
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
|
||||
|
||||
@@ -19,6 +19,11 @@ export interface INodeDuplicateList {
|
||||
duplicates: string[];
|
||||
}
|
||||
|
||||
export interface INodeLimitedList {
|
||||
nodes: string[];
|
||||
truncated: number;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class NavNodeViewService {
|
||||
get tabs(): NavNodeTransformView[] {
|
||||
@@ -41,12 +46,12 @@ export class NavNodeViewService {
|
||||
.map(transform => transform.transformer);
|
||||
}
|
||||
|
||||
private transformers: INavNodeFolderTransform[];
|
||||
private duplicationNotify: Set<string>;
|
||||
private readonly transformers: INavNodeFolderTransform[];
|
||||
private readonly duplicationNotify: Set<string>;
|
||||
|
||||
constructor(
|
||||
private navTreeResource: NavTreeResource,
|
||||
private notificationService: NotificationService
|
||||
private readonly navTreeResource: NavTreeResource,
|
||||
private readonly notificationService: NotificationService
|
||||
) {
|
||||
this.transformers = [];
|
||||
this.duplicationNotify = new Set();
|
||||
@@ -72,9 +77,15 @@ export class NavNodeViewService {
|
||||
getFolders(nodeId: string): string[] | undefined {
|
||||
const children = this.navTreeResource.get(nodeId);
|
||||
|
||||
if (!children) {
|
||||
return;
|
||||
}
|
||||
|
||||
const limited = this.limit(children);
|
||||
|
||||
return this.transformations.reduce(
|
||||
(children, transform) => transform(nodeId, children),
|
||||
children
|
||||
limited.nodes as string[] | undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,6 +93,21 @@ export class NavNodeViewService {
|
||||
this.transformers.push(transform);
|
||||
}
|
||||
|
||||
limit(nodes: string[]): INodeLimitedList {
|
||||
let truncated = 0;
|
||||
|
||||
if (nodes.length > this.navTreeResource.childrenLimit) {
|
||||
truncated = nodes.length - this.navTreeResource.childrenLimit;
|
||||
}
|
||||
|
||||
nodes = nodes.slice(0, this.navTreeResource.childrenLimit);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
truncated,
|
||||
};
|
||||
}
|
||||
|
||||
filterDuplicates(nodes: string[]): INodeDuplicateList {
|
||||
const nextChildren: string[] = [];
|
||||
const duplicates: string[] = [];
|
||||
|
||||
@@ -473,6 +473,6 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
|
||||
|
||||
navNodeInfo.hasChildren = navNodeInfo.hasChildren && navNodeChildren.length > 0;
|
||||
|
||||
return { navNodeChildren: navNodeChildren.slice(0, this.childrenLimit), navNodeInfo, parentPath };
|
||||
return { navNodeChildren, navNodeInfo, parentPath };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { NavigationTreeService } from '../NavigationTree/NavigationTreeService';
|
||||
import { ROOT_NODE_PATH } from './NodesManager/NavNodeInfoResource';
|
||||
import { NavNodeViewService } from './NodesManager/NavNodeView/NavNodeViewService';
|
||||
import { NavTreeResource } from './NodesManager/NavTreeResource';
|
||||
|
||||
interface Hook {
|
||||
@@ -23,11 +24,16 @@ interface Hook {
|
||||
}
|
||||
|
||||
export function useChildren(navNodeId = ROOT_NODE_PATH): Hook {
|
||||
const navNodeViewService = useService(NavNodeViewService);
|
||||
const navTreeService = useService(NavigationTreeService);
|
||||
const navTreeResource = useService(NavTreeResource);
|
||||
const children = navTreeService.getChildren(navNodeId);
|
||||
let children = navTreeService.getChildren(navNodeId);
|
||||
const exception = navTreeResource.getException(navNodeId);
|
||||
|
||||
if (children) {
|
||||
children = navNodeViewService.limit(children).nodes;
|
||||
}
|
||||
|
||||
const deps = [navNodeId];
|
||||
|
||||
const isLoading = useCallback(() => navTreeResource.isDataLoading(navNodeId), deps);
|
||||
|
||||
@@ -33,7 +33,7 @@ interface IServiceDescriptionLink extends IServiceDescriptionLinkOptions {
|
||||
export class AuthProviderService {
|
||||
readonly requestAuthProvider: IExecutor<ObjectOrigin>;
|
||||
|
||||
private serviceDescriptionLinker: IServiceDescriptionLink[]; // TODO: probably should be replaced by PlaceholderContainer
|
||||
private readonly serviceDescriptionLinker: IServiceDescriptionLink[]; // TODO: probably should be replaced by PlaceholderContainer
|
||||
|
||||
constructor(
|
||||
private readonly authProvidersResource: AuthProvidersResource
|
||||
|
||||
@@ -10,7 +10,7 @@ import styled, { css } from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { baseFormControlStyles } from '../baseFormControlStyles';
|
||||
import { baseFormControlStyles, baseValidFormControlStyles } from '../baseFormControlStyles';
|
||||
import { isControlPresented } from '../isControlPresented';
|
||||
import { Checkbox, CheckboxBaseProps, CheckboxType, ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox';
|
||||
|
||||
@@ -40,7 +40,7 @@ export const FieldCheckbox: CheckboxType = function FieldCheckbox({
|
||||
className,
|
||||
...rest
|
||||
}: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps<any>)) {
|
||||
const styles = useStyles(baseFormControlStyles, style);
|
||||
const styles = useStyles(baseFormControlStyles, baseValidFormControlStyles, style);
|
||||
|
||||
if (rest.autoHide && !isControlPresented(rest.name, rest.state)) {
|
||||
return null;
|
||||
|
||||
@@ -12,7 +12,7 @@ import styled, { css } from 'reshadow';
|
||||
import { useStyles, composes } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { filterLayoutFakeProps } from '../../Containers/filterLayoutFakeProps';
|
||||
import { baseFormControlStyles } from '../baseFormControlStyles';
|
||||
import { baseFormControlStyles, baseValidFormControlStyles } from '../baseFormControlStyles';
|
||||
import { isControlPresented } from '../isControlPresented';
|
||||
import type { ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox';
|
||||
import { useCheckboxState } from './useCheckboxState';
|
||||
@@ -122,6 +122,7 @@ export const Switch: SwitchType = observer(function Switch({
|
||||
rest = filterLayoutFakeProps(rest);
|
||||
const styles = useStyles(
|
||||
baseFormControlStyles,
|
||||
baseValidFormControlStyles,
|
||||
switchStyles,
|
||||
...mod.map(mod => switchMod[mod]),
|
||||
disabled && switchState.disabled,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { filterLayoutFakeProps } from '../Containers/filterLayoutFakeProps';
|
||||
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
|
||||
import { Icon } from '../Icon';
|
||||
import { IconOrImage } from '../IconOrImage';
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
|
||||
const styles = composes(
|
||||
@@ -321,7 +321,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
|
||||
|
||||
const icon = selectedItem && iconSelector?.(selectedItem);
|
||||
|
||||
return styled(useStyles(baseFormControlStyles, styles))(
|
||||
return styled(useStyles(baseFormControlStyles, baseValidFormControlStyles, styles))(
|
||||
<field className={className}>
|
||||
{children && <field-label title={title} as='label'>{children}{rest.required && ' *'}</field-label>}
|
||||
<input-box>
|
||||
@@ -357,15 +357,15 @@ export const Combobox: ComboboxType = observer(function Combobox({
|
||||
>
|
||||
{!filteredItems.length
|
||||
? (
|
||||
<MenuItem id='placeholder' disabled {...menu}>
|
||||
{translate('combobox_no_options_placeholder')}
|
||||
</MenuItem>
|
||||
)
|
||||
<MenuItem id='placeholder' disabled {...menu}>
|
||||
{translate('combobox_no_options_placeholder')}
|
||||
</MenuItem>
|
||||
)
|
||||
: (filteredItems.map((item, index) => {
|
||||
const icon = iconSelector?.(item);
|
||||
const title = titleSelector?.(item);
|
||||
const icon = iconSelector?.(item);
|
||||
const title = titleSelector?.(item);
|
||||
|
||||
return (
|
||||
return (
|
||||
<MenuItem
|
||||
key={keySelector(item, index)}
|
||||
id={keySelector(item, index)}
|
||||
@@ -381,8 +381,8 @@ export const Combobox: ComboboxType = observer(function Combobox({
|
||||
)}
|
||||
<item-value>{valueSelector(item)}</item-value>
|
||||
</MenuItem>
|
||||
);
|
||||
}))}
|
||||
);
|
||||
}))}
|
||||
</Menu>
|
||||
</input-box>
|
||||
</field>
|
||||
|
||||
@@ -10,7 +10,7 @@ import styled, { css } from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
|
||||
|
||||
const style = css`
|
||||
field-label {
|
||||
@@ -31,7 +31,7 @@ export const FormFieldDescription: React.FC<Props> = function FormFieldDescripti
|
||||
children,
|
||||
className,
|
||||
}) {
|
||||
const styles = useStyles(baseFormControlStyles, style);
|
||||
const styles = useStyles(baseFormControlStyles, baseValidFormControlStyles, style);
|
||||
|
||||
return styled(styles)(
|
||||
<field title={title} className={className}>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ComponentStyle, composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
|
||||
import { Icon } from '../Icon';
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { baseFormControlStyles, baseInvalidFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
import { isControlPresented } from './isControlPresented';
|
||||
|
||||
@@ -61,6 +61,7 @@ const INPUT_FIELD_STYLES = composes(
|
||||
`);
|
||||
|
||||
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'name' | 'value' | 'style'> & ILayoutSizeProps & {
|
||||
error?: boolean;
|
||||
description?: string;
|
||||
labelTooltip?: string;
|
||||
mod?: 'surface';
|
||||
@@ -105,6 +106,7 @@ export const InputField: InputFieldType = observer(function InputField({
|
||||
mapValue,
|
||||
children,
|
||||
className,
|
||||
error,
|
||||
description,
|
||||
labelTooltip,
|
||||
mod,
|
||||
@@ -120,7 +122,12 @@ export const InputField: InputFieldType = observer(function InputField({
|
||||
}: ControlledProps | ObjectProps<any, any>, ref: React.Ref<HTMLInputElement>) {
|
||||
const [passwordRevealed, setPasswordRevealed] = useState(false);
|
||||
const translate = useTranslate();
|
||||
const styles = useStyles(baseFormControlStyles, INPUT_FIELD_STYLES, style);
|
||||
const styles = useStyles(
|
||||
baseFormControlStyles,
|
||||
error ? baseInvalidFormControlStyles : baseValidFormControlStyles,
|
||||
INPUT_FIELD_STYLES,
|
||||
style
|
||||
);
|
||||
const context = useContext(FormContext);
|
||||
|
||||
const revealPassword = useCallback(() => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import styled, { css, use } from 'reshadow';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
|
||||
const styles = css`
|
||||
@@ -106,7 +106,7 @@ export const Textarea: TextareaType = observer(function Textarea({
|
||||
|
||||
const value = state ? state[name] : controlledValue;
|
||||
|
||||
return styled(useStyles(baseFormControlStyles, styles, style))(
|
||||
return styled(useStyles(baseFormControlStyles, baseValidFormControlStyles, styles, style))(
|
||||
<field className={className} {...use({ tiny, small, medium, large, embedded })}>
|
||||
<field-label
|
||||
as='label'
|
||||
|
||||
@@ -12,9 +12,6 @@ import { composes } from '@cloudbeaver/core-theming';
|
||||
|
||||
export const baseFormControlStyles = composes(
|
||||
css`
|
||||
field-description {
|
||||
composes: theme-text-text-hint-on-light from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
field {
|
||||
@@ -48,3 +45,21 @@ export const baseFormControlStyles = composes(
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export const baseValidFormControlStyles = composes(
|
||||
css`
|
||||
field-description {
|
||||
composes: theme-text-text-hint-on-light from global;
|
||||
}
|
||||
`,
|
||||
css``
|
||||
);
|
||||
|
||||
export const baseInvalidFormControlStyles = composes(
|
||||
css`
|
||||
field-description {
|
||||
composes: theme-text-negative from global;
|
||||
}
|
||||
`,
|
||||
css``
|
||||
);
|
||||
|
||||
@@ -193,7 +193,7 @@ export function useDataResource<
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!outdated && !refObj.firstRender) {
|
||||
if ((!outdated && !refObj.firstRender) || refObj.key === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ export function useMapResource<
|
||||
const preloaded = refObj.preloaded; // make mobx subscription
|
||||
|
||||
useEffect(() => {
|
||||
if (!preloaded || (!outdated && !refObj.firstRender)) {
|
||||
if (!preloaded || (!outdated && !refObj.firstRender) || refObj.key === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, useFocus } from '@cloudbeaver/core-blocks';
|
||||
import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, useFocus, useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { Translate, useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
import { throttleAsync } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CommonDialogWrapper } from './CommonDialog/CommonDialogWrapper';
|
||||
import type { DialogComponent } from './CommonDialogService';
|
||||
@@ -34,9 +36,10 @@ export interface RenameDialogPayload {
|
||||
bigIcon?: boolean;
|
||||
viewBox?: string;
|
||||
confirmActionText?: string;
|
||||
validation?: (name: string) => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
export const RenameDialog: DialogComponent<RenameDialogPayload, string> = function RenameDialog({
|
||||
export const RenameDialog: DialogComponent<RenameDialogPayload, string> = observer(function RenameDialog({
|
||||
payload,
|
||||
resolveDialog,
|
||||
rejectDialog,
|
||||
@@ -48,7 +51,20 @@ export const RenameDialog: DialogComponent<RenameDialogPayload, string> = functi
|
||||
const { icon, subTitle, bigIcon, viewBox, value, objectName, confirmActionText } = payload;
|
||||
const title = `${translate('ui_rename')} ${objectName}`;
|
||||
|
||||
const [name, setName] = useState(value);
|
||||
const state = useObservableRef(() => ({
|
||||
value,
|
||||
valid: true,
|
||||
validate: throttleAsync(async () => {
|
||||
state.valid = (await state.payload.validation?.(state.value)) ?? true;
|
||||
}, 300),
|
||||
}), {
|
||||
value: observable.ref,
|
||||
valid: observable.ref,
|
||||
}, {
|
||||
payload,
|
||||
});
|
||||
|
||||
const errorMessage = state.valid ? ' ' : translate('ui_rename_taken_or_invalid');
|
||||
|
||||
return styled(useStyles(style, BASE_CONTAINERS_STYLES))(
|
||||
<CommonDialogWrapper
|
||||
@@ -73,7 +89,8 @@ export const RenameDialog: DialogComponent<RenameDialogPayload, string> = functi
|
||||
<Button
|
||||
type="button"
|
||||
mod={['unelevated']}
|
||||
onClick={() => resolveDialog(name)}
|
||||
disabled={!state.valid}
|
||||
onClick={() => resolveDialog(state.value)}
|
||||
>
|
||||
<Translate token={confirmActionText || 'ui_rename'} />
|
||||
</Button>
|
||||
@@ -82,11 +99,14 @@ export const RenameDialog: DialogComponent<RenameDialogPayload, string> = functi
|
||||
fixedWidth
|
||||
onReject={rejectDialog}
|
||||
>
|
||||
<SubmittingForm ref={focusedRef} onSubmit={() => resolveDialog(name)}>
|
||||
<SubmittingForm ref={focusedRef} onSubmit={() => resolveDialog(state.value)}>
|
||||
<Container center>
|
||||
<InputField
|
||||
value={name}
|
||||
onChange={value => setName(String(value))}
|
||||
name='value'
|
||||
state={state}
|
||||
error={!state.valid}
|
||||
description={errorMessage}
|
||||
onChange={() => state.validate()}
|
||||
>
|
||||
{translate('ui_name') + ':'}
|
||||
</InputField>
|
||||
@@ -94,4 +114,4 @@ export const RenameDialog: DialogComponent<RenameDialogPayload, string> = functi
|
||||
</SubmittingForm>
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const defaultENLocale = [
|
||||
['name', 'en'],
|
||||
['ui_rename_taken_or_invalid', 'Name taken or invalid'],
|
||||
['ui_tag_new', 'New'],
|
||||
['ui_light_theme', 'Light'],
|
||||
['ui_dark_theme', 'Dark'],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const defaultITLocale = [
|
||||
['name', 'it'],
|
||||
['ui_rename_taken_or_invalid', 'Name taken or invalid'],
|
||||
['ui_tag_new', 'Nuovo'],
|
||||
['ui_light_theme', 'Light'],
|
||||
['ui_dark_theme', 'Dark'],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const defaultRULocale = [
|
||||
['name', 'ru'],
|
||||
['ui_rename_taken_or_invalid', 'Имя занято или недопустимо'],
|
||||
['ui_tag_new', 'Новый'],
|
||||
['ui_light_theme', 'Светлая'],
|
||||
['ui_dark_theme', 'Темная'],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const defaultZHLocale = [
|
||||
['name', 'zh'],
|
||||
['ui_rename_taken_or_invalid', 'Name taken or invalid'],
|
||||
['ui_tag_new', '新建'],
|
||||
['ui_light_theme', '明亮'],
|
||||
['ui_dark_theme', '黑暗'],
|
||||
|
||||
@@ -15,12 +15,12 @@ import { isArraysEqual } from '@cloudbeaver/core-utils';
|
||||
import { isNavigatorViewSettingsEqual } from './ConnectionNavigatorViewSettings';
|
||||
|
||||
@injectable()
|
||||
export class ServerConfigResource extends CachedDataResource<ServerConfig | null, void> {
|
||||
export class ServerConfigResource extends CachedDataResource<ServerConfig | null> {
|
||||
update: ServerConfigInput;
|
||||
navigatorSettingsUpdate: NavigatorSettingsInput;
|
||||
|
||||
constructor(
|
||||
private graphQLService: GraphQLService
|
||||
private readonly graphQLService: GraphQLService
|
||||
) {
|
||||
super(null);
|
||||
|
||||
@@ -43,6 +43,10 @@ export class ServerConfigResource extends CachedDataResource<ServerConfig | null
|
||||
});
|
||||
}
|
||||
|
||||
get redirectOnFederatedAuth(): boolean {
|
||||
return this.data?.redirectOnFederatedAuth ?? false;
|
||||
}
|
||||
|
||||
get serverVersion(): string {
|
||||
return this.data?.version || '';
|
||||
}
|
||||
@@ -58,7 +62,7 @@ export class ServerConfigResource extends CachedDataResource<ServerConfig | null
|
||||
get publicDisabled(): boolean {
|
||||
if (
|
||||
this.data?.configurationMode
|
||||
|| (this.data?.licenseRequired && !this.data?.licenseValid)
|
||||
|| (this.data?.licenseRequired && !this.data.licenseValid)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ query serverConfig {
|
||||
|
||||
configurationMode
|
||||
developmentMode
|
||||
redirectOnFederatedAuth
|
||||
|
||||
enabledFeatures
|
||||
enabledAuthProviders
|
||||
|
||||
@@ -1238,6 +1238,7 @@ export interface ServerConfig {
|
||||
productConfiguration: Scalars['Object'];
|
||||
productInfo: ProductInfo;
|
||||
publicCredentialsSaveEnabled?: Maybe<Scalars['Boolean']>;
|
||||
redirectOnFederatedAuth?: Maybe<Scalars['Boolean']>;
|
||||
rootURI: Scalars['String'];
|
||||
serverURL: Scalars['String'];
|
||||
services?: Maybe<Array<Maybe<WebServiceConfig>>>;
|
||||
@@ -2106,7 +2107,7 @@ export type ReadSessionLogQuery = { log: Array<{ time?: any; type: string; messa
|
||||
export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
|
||||
export type ServerConfigQuery = { serverConfig: { name: string; version: string; workspaceId: string; serverURL: string; rootURI: string; hostName: string; productConfiguration: any; supportsCustomConnections?: boolean; supportsConnectionBrowser?: boolean; supportsWorkspaces?: boolean; sessionExpireTime?: number; anonymousAccessEnabled?: boolean; adminCredentialsSaveEnabled?: boolean; publicCredentialsSaveEnabled?: boolean; licenseRequired: boolean; licenseValid: boolean; configurationMode?: boolean; developmentMode?: boolean; enabledFeatures: Array<string>; enabledAuthProviders: Array<string>; supportedLanguages: Array<{ isoCode: string; displayName?: string; nativeName?: string }>; defaultNavigatorSettings: { showSystemObjects: boolean; showUtilityObjects: boolean; showOnlyEntities: boolean; mergeEntities: boolean; hideFolders: boolean; hideSchemas: boolean; hideVirtualModel: boolean }; productInfo: { id: string; version: string; latestVersionInfo?: string; name: string; description?: string; buildTime: string; releaseTime: string; licenseInfo?: string } } };
|
||||
export type ServerConfigQuery = { serverConfig: { name: string; version: string; workspaceId: string; serverURL: string; rootURI: string; hostName: string; productConfiguration: any; supportsCustomConnections?: boolean; supportsConnectionBrowser?: boolean; supportsWorkspaces?: boolean; sessionExpireTime?: number; anonymousAccessEnabled?: boolean; adminCredentialsSaveEnabled?: boolean; publicCredentialsSaveEnabled?: boolean; licenseRequired: boolean; licenseValid: boolean; configurationMode?: boolean; developmentMode?: boolean; redirectOnFederatedAuth?: boolean; enabledFeatures: Array<string>; enabledAuthProviders: Array<string>; supportedLanguages: Array<{ isoCode: string; displayName?: string; nativeName?: string }>; defaultNavigatorSettings: { showSystemObjects: boolean; showUtilityObjects: boolean; showOnlyEntities: boolean; mergeEntities: boolean; hideFolders: boolean; hideSchemas: boolean; hideVirtualModel: boolean }; productInfo: { id: string; version: string; latestVersionInfo?: string; name: string; description?: string; buildTime: string; releaseTime: string; licenseInfo?: string } } };
|
||||
|
||||
export type SessionPermissionsQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
@@ -3338,6 +3339,7 @@ export const ServerConfigDocument = `
|
||||
licenseValid
|
||||
configurationMode
|
||||
developmentMode
|
||||
redirectOnFederatedAuth
|
||||
enabledFeatures
|
||||
enabledAuthProviders
|
||||
supportedLanguages {
|
||||
|
||||
@@ -44,6 +44,7 @@ $mdc-theme-text-colors: (
|
||||
hint: rgba(white, 0.45),
|
||||
disabled: rgba(white, 0.45),
|
||||
icon: rgba(white, 0.45),
|
||||
negative: $color-negative,
|
||||
),
|
||||
light: (
|
||||
primary: rgba(black, 0.87),
|
||||
@@ -51,6 +52,7 @@ $mdc-theme-text-colors: (
|
||||
hint: rgba(black, 0.45),
|
||||
disabled: rgba(black, 0.2),
|
||||
icon: rgba(black, 0.38),
|
||||
negative: $color-negative,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ $mdc-theme-text-colors: (
|
||||
hint: rgba(white, 0.45),
|
||||
disabled: rgba(white, 0.45),
|
||||
icon: rgba(white, 0.45),
|
||||
negative: $color-negative,
|
||||
),
|
||||
dark: (
|
||||
primary: rgba(black, 0.87),
|
||||
@@ -50,6 +51,7 @@ $mdc-theme-text-colors: (
|
||||
hint: rgba(black, 0.45),
|
||||
disabled: rgba(black, 0.2),
|
||||
icon: rgba(black, 0.38),
|
||||
negative: $color-negative,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export const Tab = observer<TabProps>(function Tab({
|
||||
const { state, getInfo, handleClose, handleOpen } = useTab(tabId, onOpen, onClose, onClick);
|
||||
const menu = useMenu({
|
||||
menu: MENU_TAB,
|
||||
context: menuContext,
|
||||
context: menuContext,
|
||||
});
|
||||
const info = getInfo();
|
||||
|
||||
@@ -51,7 +51,8 @@ export const Tab = observer<TabProps>(function Tab({
|
||||
menu.context.set(DATA_CONTEXT_TAB_ID, tabId);
|
||||
|
||||
const showMenu = menu.getItems().length > 0;
|
||||
const actionsEnabled = !!onClose || showMenu;
|
||||
const canClose = !!onClose || state.closable;
|
||||
const actionsEnabled = canClose || showMenu;
|
||||
|
||||
return styled(useStyles(style))(
|
||||
<TabContext.Provider value={tabContext}>
|
||||
@@ -72,7 +73,7 @@ export const Tab = observer<TabProps>(function Tab({
|
||||
</BaseTab>
|
||||
{actionsEnabled && (
|
||||
<tab-actions>
|
||||
{onClose && (
|
||||
{canClose && (
|
||||
<tab-action title={translate('ui_close')} onClick={handleClose}>
|
||||
<Icon name="cross-bold" viewBox="0 0 7 8" />
|
||||
</tab-action>
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
|
||||
import { useExecutor, useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
|
||||
|
||||
import type { ITabData } from '../TabsContainer/ITabsContainer';
|
||||
import { TabsContext } from '../TabsContext';
|
||||
|
||||
export function useTab(
|
||||
tabId: string,
|
||||
onOpen?: (tab: ITabData<any>) => void,
|
||||
onClose?: (tab: ITabData<any>) => void,
|
||||
onOpen?: (tab: ITabData<any>) => Promise<void> | void,
|
||||
onClose?: (tab: ITabData<any>) => Promise<void> | void,
|
||||
onClick?: (tabId: string) => void,
|
||||
) {
|
||||
const state = useContext(TabsContext);
|
||||
@@ -27,21 +27,21 @@ export function useTab(
|
||||
|
||||
useExecutor({
|
||||
executor: state.openExecutor,
|
||||
handlers: [function openHandler(data) {
|
||||
handlers: [async function openHandler(data) {
|
||||
if (tabId !== data.tabId) {
|
||||
return;
|
||||
}
|
||||
onOpen?.(data);
|
||||
await onOpen?.(data);
|
||||
}],
|
||||
});
|
||||
|
||||
useExecutor({
|
||||
executor: state.closeExecutor,
|
||||
handlers: [function closeHandler(data) {
|
||||
handlers: [async function closeHandler(data) {
|
||||
if (tabId !== data.tabId) {
|
||||
return;
|
||||
}
|
||||
onClose?.(data);
|
||||
await onClose?.(data);
|
||||
}],
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ITabsContext<T = Record<string, any>> {
|
||||
openExecutor: IExecutor<ITabData<T>>;
|
||||
closeExecutor: IExecutor<ITabData<T>>;
|
||||
lazy: boolean;
|
||||
closable: boolean;
|
||||
tabList?: string[];
|
||||
enabledBaseActions?: boolean;
|
||||
getTabInfo: (tabId: string) => ITabInfo<T> | undefined;
|
||||
|
||||
@@ -62,6 +62,8 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
}
|
||||
}
|
||||
|
||||
const closable = !!onClose;
|
||||
|
||||
const [localTabsState] = useState(() => new MetadataMap<string, any>());
|
||||
const tabsState = localState || localTabsState;
|
||||
const [closeExecutor] = useState(() => new Executor<ITabData<T>>());
|
||||
@@ -82,7 +84,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
tabsState,
|
||||
container,
|
||||
state,
|
||||
tabList
|
||||
tabList,
|
||||
});
|
||||
|
||||
if (currentTabId !== undefined) {
|
||||
@@ -197,6 +199,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
openExecutor,
|
||||
closeExecutor,
|
||||
lazy,
|
||||
closable,
|
||||
tabList,
|
||||
enabledBaseActions,
|
||||
getTabInfo,
|
||||
@@ -215,6 +218,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
closeExecutor,
|
||||
openExecutor,
|
||||
lazy,
|
||||
closable,
|
||||
tabList,
|
||||
enabledBaseActions,
|
||||
getTabInfo,
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import { AppAuthService, AuthInfoService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
|
||||
import type { DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import type { IExecutorHandler } from '@cloudbeaver/core-executor';
|
||||
import { SessionDataResource } from '@cloudbeaver/core-root';
|
||||
import { ServerConfigResource, SessionDataResource } from '@cloudbeaver/core-root';
|
||||
import { ScreenService } from '@cloudbeaver/core-routing';
|
||||
import type { ObjectOrigin } from '@cloudbeaver/core-sdk';
|
||||
import { openCenteredPopup } from '@cloudbeaver/core-utils';
|
||||
@@ -23,7 +24,7 @@ export class AuthenticationService extends Bootstrap {
|
||||
configureAuthProvider: (() => void) | null;
|
||||
configureIdentityProvider: (() => void) | null;
|
||||
|
||||
private authPromise: Promise<void> | null;
|
||||
private authPromise: Promise<DialogueStateResult | null> | null;
|
||||
|
||||
constructor(
|
||||
private readonly screenService: ScreenService,
|
||||
@@ -35,7 +36,8 @@ export class AuthenticationService extends Bootstrap {
|
||||
private readonly authProviderService: AuthProviderService,
|
||||
private readonly authProvidersResource: AuthProvidersResource,
|
||||
private readonly sessionDataResource: SessionDataResource,
|
||||
private readonly authInfoService: AuthInfoService
|
||||
private readonly authInfoService: AuthInfoService,
|
||||
private readonly serverConfigResource: ServerConfigResource,
|
||||
) {
|
||||
super();
|
||||
this.authPromise = null;
|
||||
@@ -56,10 +58,12 @@ export class AuthenticationService extends Bootstrap {
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
const userAuthConfiguration = this.authInfoService.userAuthConfigurations[0];
|
||||
if (this.authInfoService.userAuthConfigurations.length > 0) {
|
||||
const userAuthConfiguration = this.authInfoService.userAuthConfigurations[0];
|
||||
|
||||
if (userAuthConfiguration?.signOutLink) {
|
||||
this.logoutConfiguration(userAuthConfiguration.id, true);
|
||||
if (userAuthConfiguration.signOutLink) {
|
||||
this.logoutConfiguration(userAuthConfiguration.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -103,9 +107,30 @@ export class AuthenticationService extends Bootstrap {
|
||||
if (this.authPromise) {
|
||||
return this.authPromise;
|
||||
}
|
||||
|
||||
this.authPromise = this.authDialogService.showLoginForm(persistent, provider, link);
|
||||
|
||||
if (this.serverConfigResource.redirectOnFederatedAuth) {
|
||||
await this.authProvidersResource.loadAll();
|
||||
|
||||
const providers = this.authProvidersResource
|
||||
.getEnabledProviders();
|
||||
|
||||
if (providers.length === 1) {
|
||||
const configurableProvider = providers.find(provider => provider.configurable);
|
||||
|
||||
if (configurableProvider?.configurations?.length === 1) {
|
||||
const user = await this.authInfoService.sso(configurableProvider.id, configurableProvider.configurations[0]);
|
||||
|
||||
if (user) {
|
||||
this.authDialogService.closeLoginForm(this.authPromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.authPromise;
|
||||
return this.authPromise;
|
||||
} finally {
|
||||
this.authPromise = null;
|
||||
}
|
||||
@@ -140,7 +165,7 @@ export class AuthenticationService extends Bootstrap {
|
||||
|
||||
load(): void { }
|
||||
|
||||
private requestAuthProviderHandler: IExecutorHandler<ObjectOrigin> = async (data, contexts) => {
|
||||
private readonly requestAuthProviderHandler: IExecutorHandler<ObjectOrigin> = async (data, contexts) => {
|
||||
if (data.type === AUTH_PROVIDER_LOCAL_ID) {
|
||||
const provider = contexts.getContext(AuthProviderContext);
|
||||
provider.auth();
|
||||
|
||||
@@ -7,17 +7,25 @@
|
||||
*/
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
|
||||
import { AuthDialog } from './AuthDialog';
|
||||
|
||||
@injectable()
|
||||
export class AuthDialogService {
|
||||
constructor(
|
||||
private commonDialogService: CommonDialogService
|
||||
private readonly commonDialogService: CommonDialogService
|
||||
) { }
|
||||
|
||||
async showLoginForm(persistent = false, providerId: string | null = null, link?: boolean): Promise<void> {
|
||||
await this.commonDialogService.open(AuthDialog, { providerId, link }, { persistent });
|
||||
showLoginForm(
|
||||
persistent = false,
|
||||
providerId: string | null = null,
|
||||
link?: boolean
|
||||
): Promise<DialogueStateResult | null> {
|
||||
return this.commonDialogService.open(AuthDialog, { providerId, link }, { persistent });
|
||||
}
|
||||
|
||||
closeLoginForm(promise: Promise<DialogueStateResult | null>): void {
|
||||
this.commonDialogService.rejectDialog(promise);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -10,8 +10,7 @@ import { observer } from 'mobx-react-lite';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { DBObjectResource, NavTreeResource } from '@cloudbeaver/core-app';
|
||||
import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoForm, Group, useMapResource } from '@cloudbeaver/core-blocks';
|
||||
import { BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoForm, Group, useMapResource, BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
|
||||
|
||||
+4
-2
@@ -9,7 +9,7 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { DBObject, DBObjectResource, NavNodeInfoResource, NavNodeTransformViewComponent, NavTreeResource } from '@cloudbeaver/core-app';
|
||||
import { DBObject, DBObjectResource, NavNodeInfoResource, NavNodeTransformViewComponent, NavNodeViewService, NavTreeResource } from '@cloudbeaver/core-app';
|
||||
import { Loader, TextPlaceholder, useMapResource } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
@@ -33,12 +33,14 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = observer(functi
|
||||
}) {
|
||||
const translate = useTranslate();
|
||||
const nodeType = VirtualFolderUtils.getNodeType(folderId);
|
||||
const navNodeViewService = useService(NavNodeViewService);
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
const tree = useMapResource(VirtualFolderPanel, NavTreeResource, nodeId, {
|
||||
onLoad: async resource => !(await resource.preloadNodeParents(parents, nodeId)),
|
||||
});
|
||||
|
||||
const key = resourceKeyList(tree.data || []);
|
||||
const { nodes } = navNodeViewService.limit(tree.data || []);
|
||||
const key = resourceKeyList(nodes);
|
||||
const dbObject = useMapResource(VirtualFolderPanel, DBObjectResource, key, {
|
||||
async onLoad(resource: DBObjectResource) {
|
||||
const preloaded = await tree.resource.preloadNodeParents(parents, nodeId);
|
||||
|
||||
-3
@@ -9,9 +9,6 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { useChildren } from '@cloudbeaver/core-app';
|
||||
import { Loader, TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { useStyles, composes } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { ObjectPagePanelComponent } from '../ObjectPage/ObjectPage';
|
||||
|
||||
+3
-1
@@ -47,7 +47,9 @@ export const ObjectPropertyTable = observer<ObjectPropertyTableProps>(function O
|
||||
onLoad: async resource => !(await resource.preloadNodeParents(parents, objectId)),
|
||||
});
|
||||
|
||||
const { nodes, duplicates } = navNodeViewService.filterDuplicates(tree.data || []);
|
||||
const limited = navNodeViewService.limit(tree.data || []);
|
||||
|
||||
const { nodes, duplicates } = navNodeViewService.filterDuplicates(limited.nodes);
|
||||
|
||||
const key = resourceKeyList(nodes);
|
||||
const dbObject = useMapResource(ObjectPropertyTable, DBObjectResource, key, {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ConnectionSchemaManagerService,
|
||||
isObjectCatalogProvider, isObjectSchemaProvider, DATA_CONTEXT_NAV_NODE, NavigationTabsService
|
||||
} from '@cloudbeaver/core-app';
|
||||
import { ConnectionInfoResource, isConnectionProvider } from '@cloudbeaver/core-connections';
|
||||
import { isConnectionProvider } from '@cloudbeaver/core-connections';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs';
|
||||
import type { IExecutorHandler } from '@cloudbeaver/core-executor';
|
||||
@@ -20,12 +20,13 @@ import { ExtensionUtils } from '@cloudbeaver/core-extensions';
|
||||
import { ISessionAction, sessionActionContext, SessionActionService } from '@cloudbeaver/core-root';
|
||||
import { ActionService, ACTION_RENAME, DATA_CONTEXT_MENU_NESTED, menuExtractActions, MenuService, ViewService } from '@cloudbeaver/core-view';
|
||||
import { DATA_CONTEXT_CONNECTION } from '@cloudbeaver/plugin-connections';
|
||||
import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName } from '@cloudbeaver/plugin-sql-editor';
|
||||
import { DATA_CONTEXT_SQL_EDITOR_STATE } from '@cloudbeaver/plugin-sql-editor';
|
||||
|
||||
import { ACTION_SQL_EDITOR_OPEN } from './ACTION_SQL_EDITOR_OPEN';
|
||||
import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB';
|
||||
import { isSessionActionOpenSQLEditor } from './sessionActionOpenSQLEditor';
|
||||
import { SqlEditorNavigatorService } from './SqlEditorNavigatorService';
|
||||
import { SqlEditorTabService } from './SqlEditorTabService';
|
||||
|
||||
@injectable()
|
||||
export class SqlEditorBootstrap extends Bootstrap {
|
||||
@@ -39,7 +40,7 @@ export class SqlEditorBootstrap extends Bootstrap {
|
||||
private readonly menuService: MenuService,
|
||||
private readonly sessionActionService: SessionActionService,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource
|
||||
private readonly sqlEditorTabService: SqlEditorTabService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -110,14 +111,17 @@ export class SqlEditorBootstrap extends Bootstrap {
|
||||
switch (action) {
|
||||
case ACTION_RENAME: {
|
||||
const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE);
|
||||
const connection = this.connectionInfoResource.get(state.executionContext?.connectionId || '');
|
||||
|
||||
const name = getSqlEditorName(state, connection);
|
||||
const name = this.sqlEditorTabService.getName(state);
|
||||
|
||||
const result = await this.commonDialogService.open(RenameDialog, {
|
||||
value: name,
|
||||
objectName: name,
|
||||
icon: '/icons/sql_script_m.svg',
|
||||
validation: name => !this.sqlEditorTabService.sqlEditorTabs.some(tab => (
|
||||
tab.handlerState.order !== state.order
|
||||
&& this.sqlEditorTabService.getName(tab.handlerState) === name.trim()
|
||||
)),
|
||||
});
|
||||
|
||||
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed, makeObservable } from 'mobx';
|
||||
|
||||
import {
|
||||
NavigationTabsService,
|
||||
TabHandler,
|
||||
@@ -26,7 +28,7 @@ import {
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { CachedMapAllKey, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
|
||||
import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService, getSqlEditorName } from '@cloudbeaver/plugin-sql-editor';
|
||||
|
||||
import { SqlEditorPanel } from './SqlEditorPanel';
|
||||
import { SqlEditorTab } from './SqlEditorTab';
|
||||
@@ -34,6 +36,10 @@ import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey';
|
||||
|
||||
@injectable()
|
||||
export class SqlEditorTabService extends Bootstrap {
|
||||
get sqlEditorTabs(): ITab<ISqlEditorTabState>[] {
|
||||
return Array.from(this.navigationTabsService.findTabs<ISqlEditorTabState>(isSQLEditorTab));
|
||||
}
|
||||
|
||||
readonly tabHandler: TabHandler<ISqlEditorTabState>;
|
||||
|
||||
constructor(
|
||||
@@ -43,7 +49,7 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
private readonly sqlResultTabsService: SqlResultTabsService,
|
||||
private readonly connectionExecutionContextService: ConnectionExecutionContextService,
|
||||
private readonly connectionExecutionContextResource: ConnectionExecutionContextResource,
|
||||
private readonly connectionInfo: ConnectionInfoResource,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -64,6 +70,10 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
objectSchemaSetter(this.setObjectSchemaId.bind(this)),
|
||||
],
|
||||
});
|
||||
|
||||
makeObservable(this, {
|
||||
sqlEditorTabs: computed,
|
||||
});
|
||||
}
|
||||
|
||||
register(): void {
|
||||
@@ -73,6 +83,12 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
|
||||
load(): void {}
|
||||
|
||||
getName(tabState: ISqlEditorTabState): string {
|
||||
const connection = this.connectionInfoResource.get(tabState.executionContext?.connectionId || '');
|
||||
|
||||
return getSqlEditorName(tabState, connection);
|
||||
}
|
||||
|
||||
createNewEditor(
|
||||
name?: string,
|
||||
connectionId?: string,
|
||||
@@ -99,7 +115,7 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
const executionContext = this.connectionExecutionContextService.get(tab.handlerState.executionContext!.id);
|
||||
|
||||
if (!executionContext?.context) {
|
||||
if (!this.connectionInfo.has(tab.handlerState.executionContext?.connectionId || '')) {
|
||||
if (!this.connectionInfoResource.has(tab.handlerState.executionContext?.connectionId || '')) {
|
||||
this.resetConnectionInfo(tab.handlerState);
|
||||
}
|
||||
} else {
|
||||
@@ -116,7 +132,7 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
for (const tab of tabs) {
|
||||
if (
|
||||
ResourceKeyUtils.includes(key, tab.handlerState.executionContext!.id)
|
||||
&& !this.connectionInfo.has(tab.handlerState.executionContext!.connectionId)
|
||||
&& !this.connectionInfoResource.has(tab.handlerState.executionContext!.connectionId)
|
||||
) {
|
||||
this.resetConnectionInfo(tab.handlerState);
|
||||
}
|
||||
@@ -124,8 +140,7 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
}
|
||||
|
||||
private getFreeEditorId() {
|
||||
const editorTabs = this.navigationTabsService.findTabs<ISqlEditorTabState>(isSQLEditorTab);
|
||||
const ordered = Array.from(editorTabs).map(tab => tab.handlerState.order);
|
||||
const ordered = this.sqlEditorTabs.map(tab => tab.handlerState.order);
|
||||
return findMinimalFree(ordered, 1);
|
||||
}
|
||||
|
||||
@@ -150,9 +165,9 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
}
|
||||
|
||||
if (tab.handlerState.executionContext) {
|
||||
await this.connectionInfo.load(CachedMapAllKey);
|
||||
await this.connectionInfoResource.load(CachedMapAllKey);
|
||||
|
||||
if (!this.connectionInfo.has(tab.handlerState.executionContext.connectionId)) {
|
||||
if (!this.connectionInfoResource.has(tab.handlerState.executionContext.connectionId)) {
|
||||
this.resetConnectionInfo(tab.handlerState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
result: ISqlEditorResultTab;
|
||||
className?: string;
|
||||
style?: ComponentStyle;
|
||||
onClose: (tab: ITabData) => Promise<void>;
|
||||
onClose?: (tab: ITabData) => Promise<void>;
|
||||
}
|
||||
|
||||
export const SqlResultTab = observer<Props>(function SqlResultTab({
|
||||
|
||||
@@ -98,14 +98,19 @@ export const SqlResultTabs = observer<Props>(function SqlDataResult({ state, onT
|
||||
|
||||
return styled(style)(
|
||||
<wrapper>
|
||||
<TabsState currentTabId={currentId} tabList={tabList} enabledBaseActions onChange={handleSelect}>
|
||||
<TabsState
|
||||
currentTabId={currentId}
|
||||
tabList={tabList}
|
||||
enabledBaseActions
|
||||
onChange={handleSelect}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<TabList style={styles}>
|
||||
{orderedTabs.map(result => (
|
||||
<SqlResultTab
|
||||
key={result.id}
|
||||
result={result}
|
||||
style={styles}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
Reference in New Issue
Block a user