Merge branch 'devel' into feat/connection-form-new-design

This commit is contained in:
Naumov Alexey
2021-03-05 12:10:22 +03:00
44 changed files with 424 additions and 177 deletions
@@ -75,6 +75,16 @@ type WebServiceConfig {
bundleVersion: String!
}
type ProductInfo {
id: ID!
version: String!
name: String!
description: String
buildTime: String!
releaseTime: String!
licenseInfo: String
}
type ServerConfig {
name: String!
version: String!
@@ -100,6 +110,7 @@ type ServerConfig {
supportedLanguages: [ ServerLanguage! ]!
services: [ WebServiceConfig ]
productConfiguration: Object!
productInfo: ProductInfo!
defaultNavigatorSettings: NavigatorSettings!
}
@@ -0,0 +1,76 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.model;
import io.cloudbeaver.server.CBApplication;
import org.eclipse.core.runtime.Platform;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.utils.CommonUtils;
import java.text.DateFormat;
import java.util.Date;
/**
* Web server configuration
*/
public class WebProductInfo {
public WebProductInfo() {
}
@Property
public String getId() {
return CommonUtils.notEmpty(Platform.getProduct().getId());
}
@Property
public String getVersion() {
return GeneralUtils.getProductVersion().toString();
}
@Property
public String getName() {
return CommonUtils.notEmpty(Platform.getProduct().getName());
}
@Property
public String getDescription() {
return CommonUtils.notEmpty(Platform.getProduct().getDescription());
}
@Property
public String getBuildTime() {
Date buildTime = GeneralUtils.getProductBuildTime();
if (buildTime == null) {
buildTime = new Date();
}
return DateFormat.getDateInstance(DateFormat.LONG).format(buildTime);
}
@Property
public String getReleaseTime() {
return DateFormat.getDateInstance(DateFormat.LONG).format(GeneralUtils.getProductReleaseDate());
}
@Property
public String getLicenseInfo() {
return CBApplication.getInstance().getInfoDetails();
}
}
@@ -146,4 +146,9 @@ public class WebServerConfig {
return application.getAppConfiguration().getDefaultNavigatorSettings();
}
@Property
public WebProductInfo getProductInfo() {
return new WebProductInfo();
}
}
@@ -465,7 +465,7 @@ public class CBApplication extends BaseApplicationImpl {
@Override
public String getInfoDetails() {
return serverName;
return "";
}
@Override
@@ -6,15 +6,12 @@
* you may not use this file except in compliance with the License.
*/
import { computed } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import styled, { css } from 'reshadow';
import { Loader } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CoreSettingsService } from '../CoreSettingsService';
import type { NavNode } from '../shared/NodesManager/EntityTypes';
import { useChildren } from '../shared/useChildren';
import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement';
@@ -42,7 +39,7 @@ interface Props {
emptyPlaceholder: React.FC;
className?: string;
onOpen?: (node: NavNode) => Promise<void> | void;
onSelect?: (node: NavNode, multiple: boolean) => boolean;
onSelect?: (node: NavNode, multiple: boolean) => void;
isSelected?: (node: NavNode) => boolean;
}
@@ -55,10 +52,8 @@ export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
onSelect,
isSelected,
}) {
const config = useService(CoreSettingsService);
const nodeChildren = useChildren(root);
const Placeholder = emptyPlaceholder;
const limit = useMemo(() => computed(() => config.settings.getValue('app.navigationTree.childrenLimit')), [config]);
const context = useMemo<ITreeContext>(
() => ({ control, onOpen, onSelect, isSelected }),
@@ -80,7 +75,7 @@ export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
return styled(styles)(
<TreeContext.Provider value={context}>
<tree as="div" className={className}>
{nodeChildren.children.slice(0, limit.get()).map(id => (
{nodeChildren.children.map(id => (
<NavigationNodeElement key={id} nodeId={id} />
))}
<Loader loading={nodeChildren.isLoading()} overlay />
@@ -29,11 +29,11 @@ export class NavigationTreeContextMenuService {
private connectionInfoResource: ConnectionInfoResource
) { }
getMenuToken() {
getMenuToken(): string {
return NavigationTreeContextMenuService.menuToken;
}
getNodeViewMenuItemToken() {
getNodeViewMenuItemToken(): string {
return NavigationTreeContextMenuService.nodeViewMenuItemToken;
}
@@ -6,16 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { computed } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useContext, useMemo } from 'react';
import { useContext } from 'react';
import styled from 'reshadow';
import { TreeNodeContext, TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useStyles } from '@cloudbeaver/core-theming';
import { CoreSettingsService } from '../../../CoreSettingsService';
import { useChildren } from '../../../shared/useChildren';
interface Props {
@@ -29,11 +26,9 @@ export const NavigationNodeNested: React.FC<Props> = observer(function Navigatio
nodeId,
component,
}) {
const config = useService(CoreSettingsService);
const styles = useStyles(TREE_NODE_STYLES);
const context = useContext(TreeNodeContext);
const children = useChildren(nodeId);
const limit = useMemo(() => computed(() => config.settings.getValue('app.navigationTree.childrenLimit')), [config]);
if (!children.children || !context?.expanded) {
return null;
@@ -43,7 +38,7 @@ export const NavigationNodeNested: React.FC<Props> = observer(function Navigatio
return styled(styles)(
<TreeNodeNested>
{children.children.slice(0, limit.get()).map(child => <NavigationNode key={child} nodeId={child} />)}
{children.children.map(child => <NavigationNode key={child} nodeId={child} />)}
</TreeNodeNested>
);
});
@@ -27,18 +27,18 @@ interface INavigationNode {
leaf: boolean;
handleExpand: () => void;
handleOpen: () => void;
handleSelect: (isMultiple?: boolean) => boolean;
handleSelect: (isMultiple?: boolean) => void;
}
export function useNavigationNode(node: NavNode): INavigationNode {
const context = useContext(TreeContext);
const navigationTreeService = useService(NavigationTreeService);
const [processing, setProcessing] = useState(false);
const [isExpanded, switchExpand] = useState(false);
const { isLoading, isOutdated } = useNode(node.id);
const children = useChildren(node.id);
const loading = isLoading() || children.isLoading() || processing;
const isExpanded = navigationTreeService.isNodeExpanded(node.id);
let leaf = isLeaf(node) || (children.children?.length === 0 && !children.isOutdated());
let expanded = isExpanded && !leaf;
@@ -60,11 +60,11 @@ export function useNavigationNode(node: NavNode): INavigationNode {
clearTimeout(timeout);
setProcessing(false);
if (!state) {
switchExpand(false);
navigationTreeService.expandNode(node.id, false);
return;
}
}
switchExpand(!expanded);
navigationTreeService.expandNode(node.id, !expanded);
};
const handleOpen = async () => {
@@ -76,7 +76,9 @@ export function useNavigationNode(node: NavNode): INavigationNode {
}
};
const handleSelect = (multiple = false) => context?.onSelect?.(node, multiple) || false;
const handleSelect = (multiple = false) => {
context?.onSelect?.(node, multiple);
};
// TODO: probably should be refactored
useEffect(() => {
@@ -87,7 +89,7 @@ export function useNavigationNode(node: NavNode): INavigationNode {
.then(state => {
setProcessing(false);
if (!state) {
switchExpand(false);
navigationTreeService.expandNode(node.id, false);
}
});
}
@@ -97,7 +99,12 @@ export function useNavigationNode(node: NavNode): INavigationNode {
if (node && context?.isSelected?.(node)) {
context.onSelect?.(node, true);
}
}, [context, node]);
}, [context, node.id]);
useEffect(() => () => {
// TODO: seems like selection & expand should be specific for separate tree definitions
navigationTreeService.expandNode(node.id, false);
}, [node.id]);
return {
control: context?.control,
@@ -6,43 +6,91 @@
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { Subject, Observable } from 'rxjs';
import { action, computed, makeObservable } from 'mobx';
import { ConnectionAuthService } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { Executor, IExecutor } from '@cloudbeaver/core-executor';
import { ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { MetadataMap } from '@cloudbeaver/core-utils';
import type { IActiveView } from '@cloudbeaver/core-view';
import { CoreSettingsService } from '../CoreSettingsService';
import { EObjectFeature } from '../shared/NodesManager/EObjectFeature';
import { NavNodeExtensionsService } from '../shared/NodesManager/NavNodeExtensionsService';
import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
import { NavNodeManagerService } from '../shared/NodesManager/NavNodeManagerService';
import { NavTreeResource } from '../shared/NodesManager/NavTreeResource';
import { NodeManagerUtils } from '../shared/NodesManager/NodeManagerUtils';
export interface INavigationNodeMetadata {
selected: boolean;
expanded: boolean;
}
export interface INavigationNodeSelectionData {
id: string;
selected: boolean;
}
@injectable()
export class NavigationTreeService {
readonly selectedNodes = observable.array<string>([]);
readonly onNodeSelect: Observable<[string, boolean]>;
readonly navigationTreeMetadata: MetadataMap<string, INavigationNodeMetadata>;
readonly nodeSelectionTask: IExecutor<INavigationNodeSelectionData>;
private nodeSelectSubject: Subject<[string, boolean]>;
get childrenLimit(): number {
return this.coreSettingsService.settings.getValue('app.navigationTree.childrenLimit');
}
constructor(
private navNodeManagerService: NavNodeManagerService,
private notificationService: NotificationService,
private connectionAuthService: ConnectionAuthService,
private navNodeExtensionsService: NavNodeExtensionsService
private navNodeExtensionsService: NavNodeExtensionsService,
private navTreeResource: NavTreeResource,
private coreSettingsService: CoreSettingsService,
private navNodeInfoResource: NavNodeInfoResource
) {
this.nodeSelectSubject = new Subject();
this.onNodeSelect = this.nodeSelectSubject.asObservable();
makeObservable<NavigationTreeService, 'unselectAll'>(this, {
childrenLimit: computed,
unselectAll: action,
});
this.nodeSelectionTask = new Executor();
this.navigationTreeMetadata = new MetadataMap<string, INavigationNodeMetadata>(() => ({
selected: false,
expanded: false,
}));
this.getView = this.getView.bind(this);
this.navNodeInfoResource.onItemDelete.addHandler(key => {
ResourceKeyUtils.forEach(key, key => {
this.navigationTreeMetadata.delete(key);
});
});
this.navTreeResource.onItemDelete.addHandler(key => {
ResourceKeyUtils.forEach(key, key => {
this.expandNode(key, false);
});
});
}
async navToNode(id: string, parentId: string) {
getChildren(id: string): string[] | undefined {
const children = this.navTreeResource.get(id);
if (children) {
return children.slice(0, this.childrenLimit);
}
return children;
}
async navToNode(id: string, parentId: string): Promise<void> {
await this.navNodeManagerService.navToNode(id, parentId);
}
async loadNestedNodes(id = ROOT_NODE_PATH) {
async loadNestedNodes(id = ROOT_NODE_PATH): Promise<boolean> {
try {
if (this.isConnectionNode(id) && !await this.tryInitConnection(id)) {
return false;
@@ -55,39 +103,56 @@ export class NavigationTreeService {
return false;
}
selectNode(id: string, multiple?: boolean): boolean {
async selectNode(id: string, multiple?: boolean): Promise<void> {
if (!multiple) {
for (const id of this.selectedNodes) {
this.nodeSelectSubject.next([id, false]);
}
this.selectedNodes.clear();
await this.unselectAll();
}
if (!this.isNodeSelected(id)) {
this.selectedNodes.push(id);
this.nodeSelectSubject.next([id, true]);
return true;
}
this.selectedNodes.remove(id);
this.nodeSelectSubject.next([id, false]);
return false;
const metadata = this.navigationTreeMetadata.get(id);
metadata.selected = !metadata.selected;
await this.nodeSelectionTask.execute({
id,
selected: metadata.selected,
});
}
isNodeSelected(navNodeId: string) {
return this.selectedNodes.includes(navNodeId);
isNodeExpanded(navNodeId: string): boolean {
return this.navigationTreeMetadata.get(navNodeId).expanded;
}
isNodeSelected(navNodeId: string): boolean {
return this.navigationTreeMetadata.get(navNodeId).selected;
}
expandNode(navNodeId: string, state: boolean): void {
const metadata = this.navigationTreeMetadata.get(navNodeId);
metadata.expanded = state;
}
getView(): IActiveView<string> | null {
if (!this.selectedNodes.length) {
const element = Array.from(this.navigationTreeMetadata).find(([key, metadata]) => metadata.selected);
if (!element) {
return null;
}
const context = this.selectedNodes[0];
return {
context,
context: element[0],
extensions: this.navNodeExtensionsService.extensions,
};
}
private async unselectAll() {
for (const [id, metadata] of this.navigationTreeMetadata) {
metadata.selected = false;
await this.nodeSelectionTask.execute({
id,
selected: false,
});
}
}
private isConnectionNode(navNodeId: string) {
const node = this.navNodeManagerService.getNode(navNodeId);
return node?.objectFeatures.includes(EObjectFeature.dataSource);
@@ -15,7 +15,7 @@ export interface ITreeContext {
node: NavNode;
}>;
onOpen?: (node: NavNode) => Promise<void> | void;
onSelect?: (node: NavNode, multiple: boolean) => boolean;
onSelect?: (node: NavNode, multiple: boolean) => void;
isSelected?: (node: NavNode) => boolean;
}
@@ -16,7 +16,7 @@ import { NavigationTreeService } from './NavigationTreeService';
interface INavigationTree {
isSelected: (node: NavNode) => boolean;
handleOpen: (node: NavNode) => Promise<void>;
handleSelect: (node: NavNode, isMultiple: boolean) => boolean;
handleSelect: (node: NavNode, isMultiple: boolean) => void;
}
export function useNavigationTree(): INavigationTree {
@@ -13,9 +13,9 @@ import {
import { useImperativeHandle } from 'react';
import styled, { use } from 'reshadow';
import { Icon } from '@cloudbeaver/core-blocks';
import { Icon, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { useStyles } from '@cloudbeaver/core-theming';
import { EditorDialog } from './EditorDialog';
@@ -64,33 +64,39 @@ export const InlineEditor = observer<InlineEditorProps, HTMLInputElement | null>
onReject,
className,
}, ref) {
const props = useObjectRef({
onChange,
onReject,
onSave,
value,
});
const commonDialogService = useService(CommonDialogService);
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>| string) => {
const newValue = typeof event === 'string' ? event : event.target.value;
onChange(newValue);
}, [onChange]);
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
props.onChange(event.target.value);
}, []);
const handlePopup = useCallback(async () => {
const newValue = await commonDialogService.open(EditorDialog, value);
if (typeof newValue === 'string') {
handleChange(newValue);
onSave();
} else if (onReject) {
onReject();
const newValue = await commonDialogService.open(EditorDialog, props.value);
if (newValue === DialogueStateResult.Rejected || newValue === DialogueStateResult.Resolved) {
props.onReject?.();
} else {
props.onChange(newValue);
props.onSave();
}
}, [value, commonDialogService, onSave, onReject, handleChange]);
}, []);
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
switch (event.key) {
case 'Enter':
onSave();
props.onSave();
break;
case 'Escape':
onReject?.();
props.onReject?.();
break;
}
}, [onSave, onReject]);
}, []);
const inputRef = useRef<HTMLInputElement>(null);
@@ -332,20 +332,23 @@ export class NavNodeManagerService extends Bootstrap {
};
async updateRoot(): Promise<void> {
if (await this.isNavTreeEnabled()) {
const enabled = await this.isNavTreeEnabled();
if (enabled) {
await this.navTree.refresh(ROOT_NODE_PATH);
}
}
async refreshRoot(): Promise<void> {
this.navTree.delete(ROOT_NODE_PATH);
if (await this.isNavTreeEnabled()) {
const enabled = await this.isNavTreeEnabled();
if (enabled) {
await this.navTree.refresh(ROOT_NODE_PATH);
}
}
private async connectionCreateHandler(connection: Connection) {
if (!await this.isNavTreeEnabled()) {
const enabled = await this.isNavTreeEnabled();
if (!enabled) {
return;
}
@@ -360,7 +363,8 @@ export class NavNodeManagerService extends Bootstrap {
}
private async connectionRefreshHandler(connections: Connection[]) {
if (!await this.isNavTreeEnabled()) {
const enabled = await this.isNavTreeEnabled();
if (!enabled) {
return;
}
@@ -368,7 +372,8 @@ export class NavNodeManagerService extends Bootstrap {
}
private async connectionUpdateHandler(key: ResourceKey<string>) {
if (!await this.isNavTreeEnabled()) {
const enabled = await this.isNavTreeEnabled();
if (!enabled) {
return;
}
@@ -430,7 +435,8 @@ export class NavNodeManagerService extends Bootstrap {
}
private async isNavTreeEnabled() {
if (!await this.permissionsService.hasAsync(EPermission.public)) {
const active = await this.permissionsService.hasAsync(EPermission.public);
if (!active) {
return false;
}
@@ -10,6 +10,7 @@ import { useCallback } from 'react';
import { useService } from '@cloudbeaver/core-di';
import { NavigationTreeService } from '../NavigationTree/NavigationTreeService';
import { ROOT_NODE_PATH } from './NodesManager/NavNodeInfoResource';
import { NavTreeResource } from './NodesManager/NavTreeResource';
@@ -21,8 +22,9 @@ interface Hook {
}
export function useChildren(navNodeId = ROOT_NODE_PATH): Hook {
const navTreeService = useService(NavigationTreeService);
const navTreeResource = useService(NavTreeResource);
const children = navTreeResource.get(navNodeId);
const children = navTreeService.getChildren(navNodeId);
const deps = [navNodeId];
+39 -1
View File
@@ -7,9 +7,12 @@
*/
import styled, { css } from 'reshadow';
import { use } from 'reshadow';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { Loader } from './Loader/Loader';
const buttonStyles = composes(
css`
Button {
@@ -22,7 +25,31 @@ const buttonStyles = composes(
css`
Button {
display: flex;
& Loader, & button-label {
transition: opacity cubic-bezier(0.4, 0.0, 0.2, 1) 0.3s;
}
& Loader {
position: absolute;
opacity: 0;
}
& button-label {
opacity: 1;
}
&[|loading] {
& Loader {
opacity: 1;
}
& button-label {
opacity: 0;
}
}
}
`
);
@@ -61,6 +88,7 @@ type ButtonProps = (
React.ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement>
& React.LinkHTMLAttributes<HTMLLinkElement | HTMLButtonElement>
) & {
loading?: boolean;
mod?: Array<keyof typeof buttonMod>;
tag?: 'button' | 'a';
href?: string;
@@ -72,10 +100,20 @@ export const Button: React.FC<ButtonProps> = function Button({
children,
mod,
tag = 'button',
disabled = false,
loading,
...rest
}) {
if (loading) {
disabled = true;
}
const Button = tag;
return styled(useStyles(buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))(
<Button {...rest}><ripple as="div" />{children}</Button>
<Button {...rest} disabled={disabled} {...use({ loading })}>
<ripple as="div" />
<button-label as='div'>{children}</button-label>
<Loader small />
</Button>
);
};
@@ -7,8 +7,8 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useContext } from 'react';
import styled, { use } from 'reshadow';
import { useCallback, useContext, useState } from 'react';
import { use } from 'reshadow';
import type { IGridItemsLayoutProps } from '../../Containers/ILayoutContainerProps';
import { FormContext } from '../FormContext';
@@ -62,6 +62,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
onChange,
...rest
}: CheckboxControlledProps | CheckboxObjectProps<any, any>) {
const [count, refresh] = useState(0);
const context = useContext(FormContext);
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
if (state) {
@@ -84,7 +85,8 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
if (context) {
context.onChange(event.target.checked, name);
}
}, [state, name, value, onChange, context]);
refresh(count + 1);
}, [state, name, value, onChange, context, count]);
if (autoHide && !isControlPresented(name, state)) {
return null;
@@ -104,7 +106,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
checked = checked.includes(value);
}
return styled()(
return (
<CheckboxMarkup
{...rest}
name={name}
@@ -11,11 +11,13 @@ import { useCallback, forwardRef, useMemo } from 'react';
import { FormContext } from './FormContext';
type FormDetailedProps = Omit<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, 'onChange'> & {
disabled?: boolean;
onChange?: (value: string | number | boolean | null | undefined, name: string | undefined) => void;
};
export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(function SubmittingForm(
{
disabled,
children,
onSubmit,
onChange = () => {},
@@ -34,9 +36,11 @@ export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(fun
return (
<form {...rest} ref={ref} onSubmit={handleSubmit}>
<FormContext.Provider value={context}>
{children}
</FormContext.Provider>
<fieldset disabled={disabled} className={rest.className}>
<FormContext.Provider value={context}>
{children}
</FormContext.Provider>
</fieldset>
<button type="submit" hidden />
</form>
);
@@ -21,7 +21,7 @@ interface Props {
leaf?: boolean;
className?: string;
onExpand?: () => void;
onSelect?: (multiple?: boolean) => boolean;
onSelect?: (multiple?: boolean) => void;
onOpen?: () => void;
}
@@ -42,7 +42,7 @@ export const TreeNode: React.FC<Props> = memo(function TreeNode({
}, []);
const handleSelect = useCallback(
(multiple?: boolean): boolean => handlersRef.current.onSelect?.(multiple) || false,
(multiple?: boolean): void => handlersRef.current.onSelect?.(multiple),
[]
);
@@ -13,7 +13,7 @@ export interface ITreeNodeContext {
loading: boolean;
selected: boolean;
leaf: boolean;
select: (multiple?: boolean) => boolean;
select: (multiple?: boolean) => void;
expand: () => void;
open: () => void;
}
@@ -43,7 +43,8 @@ export const TreeNodeExpand: React.FC<Props> = function TreeNodeExpand({
throw new Error('Context not provided');
}
const handleExpand = () => {
const handleExpand = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
if (!context.leaf && !leaf) {
context.expand();
}
@@ -22,10 +22,12 @@ const styles = css`
`;
interface Props {
disabled?: boolean;
className?: string;
}
export const TreeNodeSelect: React.FC<Props> = function TreeNodeSelect({
disabled,
className,
}) {
const context = useContext(TreeNodeContext);
@@ -45,7 +47,7 @@ export const TreeNodeSelect: React.FC<Props> = function TreeNodeSelect({
return styled(styles)(
<div className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
<Checkbox checked={context.selected} onChange={() => context.select(true)} />
<Checkbox checked={context.selected} disabled={disabled} onChange={() => context.select(true)} />
</div>
);
};
@@ -20,6 +20,7 @@ import {
ResourceKeyUtils,
DatabaseConnectionFragment,
GetConnectionsQueryVariables,
TestConnectionMutation,
} from '@cloudbeaver/core-sdk';
import { MetadataMap } from '@cloudbeaver/core-utils';
@@ -122,10 +123,12 @@ export class ConnectionsResource extends CachedMapResource<string, DatabaseConne
return observedConnection;
}
async test(config: ConnectionConfig): Promise<void> {
await this.graphQLService.sdk.testConnection({
async test(config: ConnectionConfig): Promise<TestConnectionMutation['connection']> {
const { connection } = await this.graphQLService.sdk.testConnection({
config,
});
return connection;
}
async update(id: string, config: ConnectionConfig): Promise<DatabaseConnection> {
@@ -78,8 +78,11 @@ export class ConnectionOptionsTabService extends Bootstrap {
status.info(`Connection ${connection.name} created`);
}
} else {
await this.connectionsResource.test(config);
const info = await this.connectionsResource.test(config);
status.info('Connection is established');
status.info('Client version: ' + info.clientVersion);
status.info('Server version: ' + info.serverVersion);
status.info('Connection time: ' + info.connectTime);
}
} else {
if (submitType === 'submit') {
@@ -92,8 +95,11 @@ export class ConnectionOptionsTabService extends Bootstrap {
status.info(`Connection ${connection.name} created`);
}
} else {
await this.connectionInfoResource.testConnection(config);
const info = await this.connectionInfoResource.testConnection(config);
status.info('Connection is established');
status.info('Client version: ' + info.clientVersion);
status.info('Server version: ' + info.serverVersion);
status.info('Connection time: ' + info.connectTime);
}
}
} catch (exception) {
@@ -82,7 +82,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormTabProps> = obse
data.config.connectionId = data.info.id;
data.config.name = data.info.name.trim();
data.config.name = data.info.name;
data.config.description = data.info.description;
data.config.template = data.info.template;
data.config.driverId = data.info.driverId;
@@ -99,13 +99,17 @@ export const Options: TabContainerPanelComponent<IConnectionFormTabProps> = obse
data.config.authModelId = data.info.authModel;
data.config.saveCredentials = data.info.saveCredentials;
for (const property of data.info.authProperties) {
if (!property.features.includes('password')) {
data.config.credentials[property.id!] = property.value;
if (data.info.authProperties) {
for (const property of data.info.authProperties) {
if (!property.features.includes('password')) {
data.config.credentials[property.id!] = property.value;
}
}
}
data.config.providerProperties = { ...data.info.providerProperties };
if (data.info.providerProperties) {
data.config.providerProperties = { ...data.info.providerProperties };
}
}));
const optionsHook = useOptions({ data, form: form.form, options });
const { credentialsSavingEnabled } = useAdministrationSettings();
@@ -114,11 +118,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormTabProps> = obse
DBDriverResource,
{ key: data.config.driverId || null, includes: ['includeProviderProperties'] },
{
onLoad: async resource => {
if (data.availableDrivers && data.availableDrivers.length > 1) {
await resource.load(resourceKeyList(data.availableDrivers), ['includeProviderProperties']);
}
onLoad: async () => {
if (!data.config.driverId && data.info) {
data.info.authModel = undefined;
}
@@ -22,6 +22,7 @@ import {
GetUserConnectionsQueryVariables,
ResourceKey,
ResourceKeyUtils,
TestConnectionMutation,
} from '@cloudbeaver/core-sdk';
import { ConnectionsResource, DatabaseConnection } from './Administration/ConnectionsResource';
@@ -124,10 +125,12 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
return this.add(connection);
}
async testConnection(config: ConnectionConfig): Promise<void> {
await this.graphQLService.sdk.testConnection({
async testConnection(config: ConnectionConfig): Promise<TestConnectionMutation['connection']> {
const { connection } = await this.graphQLService.sdk.testConnection({
config,
});
return connection;
}
async createFromNode(nodeId: string, nodeName: string): Promise<Connection> {
@@ -41,9 +41,10 @@ export class NetworkHandlerResource extends CachedMapResource<string, NetworkHan
async test(config: NetworkHandlerConfigInput): Promise<void> {
try {
await this.graphQLService.sdk.testNetworkHandler({ config });
const { info } = await this.graphQLService.sdk.testNetworkHandler({ config });
this.notificationService.logSuccess({
title: 'connections_network_handler_test_success',
message: 'Client version: ' + info.clientVersion + '\nServer version: ' + info.serverVersion,
});
} catch (exception) {
this.notificationService.logException(exception, 'connections_network_handler_test_fail');
@@ -7,7 +7,7 @@
* you may not use this file except in compliance with the License.
*/
import { useCallback, useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import type { IExecutor } from './IExecutor';
import type { IExecutorHandlersCollection } from './IExecutorHandlersCollection';
@@ -17,24 +17,28 @@ export function useFormValidator(
ref: React.RefObject<HTMLFormElement>,
callback?: () => void
): void {
const callbackRef = useRef(callback);
callbackRef.current = callback;
const validate = useCallback(() => {
ref.current?.checkValidity();
ref.current?.reportValidity();
if (callbackRef.current) {
callbackRef.current();
}
}, [ref]);
const callbackRef = useRef({
callback,
ref,
});
callbackRef.current.callback = callback;
callbackRef.current.ref = ref;
useEffect(() => {
if (!validationTask) {
return;
}
function validate() {
if (callbackRef.current) {
callbackRef.current.ref.current?.checkValidity();
callbackRef.current.ref.current?.reportValidity();
callbackRef.current.callback?.();
}
}
validationTask.addHandler(validate);
return () => validationTask.removeHandler(validate);
}, [validationTask, validate]);
}, [validationTask]);
}
@@ -24,6 +24,6 @@ export class ServerConfigResource extends CachedDataResource<ServerConfig | null
protected async loader(): Promise<ServerConfig> {
const { serverConfig } = await this.graphQLService.sdk.serverConfig();
return serverConfig;
return serverConfig as ServerConfig;
}
}
@@ -1,5 +1,13 @@
mutation testConnection($config: ConnectionConfig!) {
testConnection(config: $config) {
connection: testConnection(config: $config) {
id
connectTime
connectionError {
message
errorCode
stackTrace
}
serverVersion
clientVersion
}
}
@@ -1,5 +1,5 @@
mutation testNetworkHandler($config: NetworkHandlerConfigInput!) {
testNetworkHandler(config: $config) {
info: testNetworkHandler(config: $config) {
message
clientVersion
serverVersion
+28 -4
View File
@@ -460,6 +460,16 @@ export interface WebServiceConfig {
bundleVersion: Scalars['String'];
}
export interface ProductInfo {
id: Scalars['ID'];
version: Scalars['String'];
name: Scalars['String'];
description?: Maybe<Scalars['String']>;
buildTime: Scalars['String'];
releaseTime: Scalars['String'];
licenseInfo?: Maybe<Scalars['String']>;
}
export interface ServerConfig {
name: Scalars['String'];
version: Scalars['String'];
@@ -479,6 +489,7 @@ export interface ServerConfig {
supportedLanguages: ServerLanguage[];
services?: Maybe<Array<Maybe<WebServiceConfig>>>;
productConfiguration: Scalars['Object'];
productInfo: ProductInfo;
defaultNavigatorSettings: NavigatorSettings;
}
@@ -1214,13 +1225,18 @@ export type TestConnectionMutationVariables = Exact<{
config: ConnectionConfig;
}>;
export interface TestConnectionMutation { testConnection: Pick<ConnectionInfo, 'id'> }
export interface TestConnectionMutation {
connection: (
Pick<ConnectionInfo, 'id' | 'connectTime' | 'serverVersion' | 'clientVersion'>
& { connectionError?: Maybe<Pick<ServerError, 'message' | 'errorCode' | 'stackTrace'>> }
);
}
export type TestNetworkHandlerMutationVariables = Exact<{
config: NetworkHandlerConfigInput;
}>;
export interface TestNetworkHandlerMutation { testNetworkHandler: Pick<NetworkEndpointInfo, 'message' | 'clientVersion' | 'serverVersion'> }
export interface TestNetworkHandlerMutation { info: Pick<NetworkEndpointInfo, 'message' | 'clientVersion' | 'serverVersion'> }
export type UpdateConnectionMutationVariables = Exact<{
config: ConnectionConfig;
@@ -2046,14 +2062,22 @@ export const SetConnectionNavigatorSettingsDocument = `
${DatabaseConnectionFragmentDoc}`;
export const TestConnectionDocument = `
mutation testConnection($config: ConnectionConfig!) {
testConnection(config: $config) {
connection: testConnection(config: $config) {
id
connectTime
connectionError {
message
errorCode
stackTrace
}
serverVersion
clientVersion
}
}
`;
export const TestNetworkHandlerDocument = `
mutation testNetworkHandler($config: NetworkHandlerConfigInput!) {
testNetworkHandler(config: $config) {
info: testNetworkHandler(config: $config) {
message
clientVersion
serverVersion
@@ -16,6 +16,10 @@
.theme-button {
composes: mdc-button;
height: 28px;
&[disabled] {
opacity: 1;
}
}
.theme-button_ripple {
composes: mdc-button__ripple;
@@ -7,7 +7,17 @@
*/
@mixin form-controls() {
input {
:global(fieldset) {
padding: 0;
border: none;
margin: 0;
&:global([disabled]) {
opacity: 1;
}
}
input:not([role="new"]) {
@include mdc-theme-prop(color, on-secondary, false);
@include mdc-theme-prop(background-color, secondary, false);
box-sizing: border-box;
@@ -45,7 +45,7 @@ export const User: React.FC<Props> = observer(function User({ user, selectable }
</TableColumnValue>
<TableColumnValue expand>{user.userId}</TableColumnValue>
<TableColumnValue>{user.grantedRoles.join(', ')}</TableColumnValue>
<TableColumnValue>
<TableColumnValue flex>
<Placeholder container={usersAdministrationService.userDetailsInfoPlaceholder} context={user} />
</TableColumnValue>
</TableItem>
@@ -60,7 +60,8 @@ export class AuthenticationService extends Bootstrap {
}
private async requireAuthentication() {
if (!await this.appAuthService.isAuthNeeded()) {
const authNeeded = await this.appAuthService.isAuthNeeded();
if (!authNeeded) {
return;
}
@@ -1,30 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { ConnectionConfig, GraphQLService } from '@cloudbeaver/core-sdk';
@injectable()
export class CustomConnectionService {
constructor(
private graphQLService: GraphQLService,
private connectionInfoResource: ConnectionInfoResource
) {
}
async createConnectionAsync(config: ConnectionConfig): Promise<Connection> {
return this.connectionInfoResource.createConnection(config);
}
async testConnectionAsync(config: ConnectionConfig): Promise<void> {
await this.graphQLService.sdk.testConnection({
config,
});
}
}
@@ -9,7 +9,6 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { CustomConnectionPluginBootstrap } from './CustomConnectionPluginBootstrap';
import { CustomConnectionService } from './CustomConnectionService';
import { LocaleService } from './LocaleService';
export const customConnectionPluginManifest: PluginManifest = {
@@ -18,7 +17,6 @@ export const customConnectionPluginManifest: PluginManifest = {
},
providers: [
CustomConnectionService,
LocaleService,
CustomConnectionPluginBootstrap,
],
@@ -62,13 +62,7 @@ export const PublicConnectionForm: React.FC = observer(function PublicConnection
const service = useService(PublicConnectionFormService);
const close = useCallback(() => service.close(), []);
const save = useCallback((config: ConnectionConfig) => {
if (config.connectionId) {
service.change({ connectionId: config.connectionId });
} else {
service.close(true);
}
}, []);
const save = useCallback(() => service.close(true), []);
return styled(styles)(
<Loader loading={service.dataOptions !== null}>
@@ -33,7 +33,7 @@ export class PublicConnectionFormBootstrap extends Bootstrap {
return context.contextType === NavigationTreeContextMenuService.nodeContextType
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
},
title: 'Edit',
title: 'connections_public_connection_edit_menu_item_title',
order: 2,
onClick: context => {
const node = context.data;
@@ -1,4 +1,5 @@
export default [
['connections_public_connection_edit_menu_item_title', 'Edit Connection'],
['connections_public_connection_edit_cancel_title', 'Cancel confirmation'],
['connections_public_connection_edit_cancel_message', 'You\'re going to cancel connection changes. Unsaved changes will be lost. Are you sure?'],
];
@@ -1,4 +1,5 @@
export default [
['connections_public_connection_edit_menu_item_title', 'Изменить подключение'],
['connections_public_connection_edit_cancel_title', 'Отмена редактирования'],
['connections_public_connection_edit_cancel_message', 'Вы собираетесь закрыть редактор, несохраненные изменения не будут применены. Вы уверены?'],
];
@@ -89,8 +89,10 @@ export const CellEditor = observer<Pick<EditorProps<any, any>, 'rowIdx' | 'row'
const type = typeof tableDataContext?.getCellValue(rowIdx, column.key) === 'number' ? 'number' : 'text';
const handleSave = () => {
dataGridContext.model.source.getEditor(dataGridContext.resultIndex)
.setCell(rowIdx, Number(column.key), Number(value));
if (type === 'number') {
const editor = dataGridContext.model.source.getEditor(dataGridContext.resultIndex);
editor.setCell(rowIdx, Number(column.key), Number(editor.getCell(rowIdx, Number(column.key))));
}
onClose(false);
};
const handleReject = () => {
@@ -69,7 +69,7 @@ export class DatabaseDataEditor<TResult extends IDatabaseDataResult> implements
set: this.set.bind(this, result),
setCell: this.setCell.bind(this, result),
get: this.get.bind(this, result),
getCell: this.get.bind(this, result),
getCell: this.getCell.bind(this, result),
isCellEdited: this.isCellEdited.bind(this, result),
isEdited: this.isResultEdited.bind(this, result),
isRowEdited: this.isRowEdited.bind(this, result),
@@ -7,10 +7,11 @@
*/
import { NavigationTabsService } from '@cloudbeaver/core-app';
import { ConnectionsManagerService, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { ConnectionsManagerService } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { IExecutor, Executor, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { NavigationService } from '@cloudbeaver/core-ui';
import type { ISqlEditorTabState } from './ISqlEditorTabState';
import { SqlEditorTabService, isSQLEditorTab } from './SqlEditorTabService';
@@ -49,13 +50,14 @@ export class SqlEditorNavigatorService {
private navigationTabsService: NavigationTabsService,
private connectionsManagerService: ConnectionsManagerService,
private notificationService: NotificationService,
private connectionInfoResource: ConnectionInfoResource,
private sqlEditorTabService: SqlEditorTabService
private sqlEditorTabService: SqlEditorTabService,
navigationService: NavigationService
) {
this.navigator = new Executor<SQLCreateAction | SQLEditorAction>(
null,
(active, current) => active.type === current.type
)
.before(navigationService.navigationTask)
.addHandler(this.navigateHandler.bind(this));
this.connectionsManagerService.onCloseConnection.subscribe(this.handleConnectionClose.bind(this));
}