Merge remote-tracking branch 'origin' into feature/CB-1594

This commit is contained in:
Alexander Skoblikov
2022-02-08 15:38:17 +03:00
58 changed files with 445 additions and 142 deletions
+9
View File
@@ -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();
@@ -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
View File
@@ -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,
@@ -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;
};
}
@@ -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> {
@@ -8,6 +8,7 @@
export interface INavigationTreeUserSettings {
filter: boolean;
filterAll: boolean;
saveExpanded: boolean;
folders: boolean;
}
@@ -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}
@@ -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
+3 -1
View File
@@ -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,
),
);
+4 -3
View File
@@ -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);
}
}
@@ -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';
@@ -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);
@@ -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';
@@ -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>