mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #309 from dbeaver/feat/system-objects
Feat/system objects
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { ConnectionInfoResource, isSimpleNavigatorView } from '@cloudbeaver/core-connections';
|
||||
import { ConnectionInfoResource, CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual, NavigatorViewSettings } from '@cloudbeaver/core-connections';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ContextMenuService, IMenuPanel } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
@@ -46,23 +46,25 @@ export class NavigationTreeContextMenuService {
|
||||
});
|
||||
}
|
||||
|
||||
private isConnectionSimpleView(nodeId: string): boolean {
|
||||
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(nodeId);
|
||||
private getConnectionFromNodeId(nodeId: string) {
|
||||
return this.connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(nodeId));
|
||||
}
|
||||
|
||||
const connection = this.connectionInfoResource.get(connectionId);
|
||||
private isSimpleNavigatorView(nodeId: string) {
|
||||
const currentSettings = this.getConnectionFromNodeId(nodeId)?.navigatorSettings;
|
||||
|
||||
if (!connection) {
|
||||
if (!currentSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isSimpleNavigatorView(connection.navigatorSettings);
|
||||
return isNavigatorViewSettingsEqual(currentSettings, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple);
|
||||
}
|
||||
|
||||
private async changeConnectionView(nodeId: string, simple: boolean) {
|
||||
private async changeConnectionView(nodeId: string, settings: NavigatorViewSettings) {
|
||||
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(nodeId);
|
||||
|
||||
try {
|
||||
await this.connectionInfoResource.changeConnectionView(connectionId, simple);
|
||||
await this.connectionInfoResource.changeConnectionView(connectionId, settings);
|
||||
await this.navNodeManagerService.refreshTree(nodeId);
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception);
|
||||
@@ -88,12 +90,15 @@ export class NavigationTreeContextMenuService {
|
||||
{
|
||||
id: 'simple',
|
||||
title: 'app_navigationTree_connection_view_option_simple',
|
||||
isDisabled: context => this.isConnectionSimpleView(context.data.id),
|
||||
type: 'radio',
|
||||
isChecked: context => this.isSimpleNavigatorView(context.data.id),
|
||||
isPresent(context) {
|
||||
return context.contextType === NavigationTreeContextMenuService.nodeContextType
|
||||
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
|
||||
},
|
||||
onClick: async context => await this.changeConnectionView(context.data.id, true),
|
||||
onClick: async context =>
|
||||
await this.changeConnectionView(context.data.id, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple),
|
||||
|
||||
}
|
||||
);
|
||||
this.contextMenuService.addMenuItem<NavNode>(
|
||||
@@ -101,12 +106,40 @@ export class NavigationTreeContextMenuService {
|
||||
{
|
||||
id: 'advanced',
|
||||
title: 'app_navigationTree_connection_view_option_advanced',
|
||||
isDisabled: context => !this.isConnectionSimpleView(context.data.id),
|
||||
type: 'radio',
|
||||
isChecked: context => !this.isSimpleNavigatorView(context.data.id),
|
||||
separator: true,
|
||||
isPresent(context) {
|
||||
return context.contextType === NavigationTreeContextMenuService.nodeContextType
|
||||
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
|
||||
},
|
||||
onClick: async context => await this.changeConnectionView(context.data.id, false),
|
||||
onClick: async context =>
|
||||
await this.changeConnectionView(context.data.id, CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced),
|
||||
}
|
||||
);
|
||||
|
||||
this.contextMenuService.addMenuItem<NavNode>(
|
||||
this.getNodeViewMenuItemToken(),
|
||||
{
|
||||
id: 'systemObjects',
|
||||
title: 'app_navigationTree_connection_view_option_showSystemObjects',
|
||||
type: 'checkbox',
|
||||
isChecked: context => !!this.getConnectionFromNodeId(context.data.id)?.navigatorSettings.showSystemObjects,
|
||||
isPresent(context) {
|
||||
return context.contextType === NavigationTreeContextMenuService.nodeContextType
|
||||
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
|
||||
},
|
||||
onClick: async context => {
|
||||
const currentSettings = this.getConnectionFromNodeId(context.data.id)?.navigatorSettings;
|
||||
if (!currentSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await this.changeConnectionView(context.data.id, {
|
||||
...currentSettings,
|
||||
showSystemObjects: !currentSettings.showSystemObjects,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export default [
|
||||
['app_navigationTree_connection_view', 'Connection view'],
|
||||
['app_navigationTree_connection_view_option_simple', 'Simple'],
|
||||
['app_navigationTree_connection_view_option_advanced', 'Advanced'],
|
||||
['app_navigationTree_connection_view_option_custom', 'Custom'],
|
||||
['app_navigationTree_connection_view_option_showSystemObjects', 'Show system objects'],
|
||||
['app_navigationTree_refreshNode', 'Refresh'],
|
||||
['app_navigationTree_context_disconnect', 'Disconnect'],
|
||||
['app_shared_inlineEditor_dialog_title', 'Edit mode'],
|
||||
|
||||
@@ -16,6 +16,8 @@ export default [
|
||||
['app_navigationTree_connection_view', 'Отображение'],
|
||||
['app_navigationTree_connection_view_option_simple', 'Упрощенное'],
|
||||
['app_navigationTree_connection_view_option_advanced', 'Продвинутое'],
|
||||
['app_navigationTree_connection_view_option_custom', 'Настраиваемое'],
|
||||
['app_navigationTree_connection_view_option_showSystemObjects', 'Показывать системные объекты'],
|
||||
['app_navigationTree_refreshNode', 'Обновить'],
|
||||
['app_navigationTree_context_disconnect', 'Отключить'],
|
||||
['app_shared_inlineEditor_dialog_title', 'Редактирование'],
|
||||
|
||||
@@ -8,18 +8,20 @@
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { use } from 'reshadow';
|
||||
|
||||
import type { IGridItemsLayoutProps } from '../../Containers/LayoutProps';
|
||||
import { FormContext } from '../FormContext';
|
||||
import { isControlPresented } from '../isControlPresented';
|
||||
import { CheckboxMarkup } from './CheckboxMarkup';
|
||||
|
||||
export type CheckboxMod = 'primary' | 'small';
|
||||
|
||||
export type CheckboxBaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'type' | 'value' | 'checked'> & IGridItemsLayoutProps & {
|
||||
value?: string;
|
||||
checkboxLabel?: string;
|
||||
mod?: 'surface';
|
||||
long?: boolean;
|
||||
mod?: CheckboxMod[];
|
||||
ripple?: boolean;
|
||||
};
|
||||
|
||||
type CheckboxOnChangeEvent<T> =
|
||||
@@ -55,8 +57,9 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
|
||||
checkboxLabel,
|
||||
checked: checkedControlled,
|
||||
children,
|
||||
className,
|
||||
mod,
|
||||
ripple,
|
||||
className,
|
||||
long,
|
||||
autoHide,
|
||||
onChange,
|
||||
@@ -114,8 +117,9 @@ export const Checkbox: CheckboxType = observer(function Checkbox({
|
||||
checked={checked}
|
||||
label={checkboxLabel}
|
||||
className={className}
|
||||
mod={mod}
|
||||
ripple={ripple}
|
||||
onChange={handleChange}
|
||||
{...use({ mod })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { Composes, composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { CheckboxMod } from './Checkbox';
|
||||
|
||||
const checkboxStyles = composes(
|
||||
css`
|
||||
@@ -46,7 +48,7 @@ const checkboxStyles = composes(
|
||||
`
|
||||
);
|
||||
|
||||
const checkboxMod = {
|
||||
const checkboxMod: Record<CheckboxMod, Composes> = {
|
||||
primary: composes(
|
||||
css`
|
||||
checkbox {
|
||||
@@ -54,6 +56,25 @@ const checkboxMod = {
|
||||
}
|
||||
`
|
||||
),
|
||||
small: composes(
|
||||
css`
|
||||
checkbox {
|
||||
composes: theme-checkbox_small from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
checkbox-container {
|
||||
& checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
& checkbox-background {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
),
|
||||
};
|
||||
|
||||
const checkboxState = {
|
||||
@@ -76,12 +97,14 @@ const checkboxState = {
|
||||
interface ICheckboxMarkupProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
indeterminate?: boolean;
|
||||
ripple?: boolean;
|
||||
mod?: CheckboxMod[];
|
||||
}
|
||||
|
||||
export const CheckboxMarkup: React.FC<ICheckboxMarkupProps> = function CheckboxMarkup({
|
||||
label, className, title, ...rest
|
||||
label, className, title, mod = ['primary'], ripple = true, ...rest
|
||||
}) {
|
||||
return styled(useStyles(checkboxStyles, checkboxMod.primary, rest.disabled
|
||||
return styled(useStyles(checkboxStyles, ...(mod || []).map(mod => checkboxMod[mod]), rest.disabled
|
||||
&& checkboxState.disabled, rest.checked && checkboxState.checked))(
|
||||
<checkbox-container className={className} title={title} as='div'>
|
||||
<checkbox as='div'>
|
||||
@@ -92,7 +115,9 @@ export const CheckboxMarkup: React.FC<ICheckboxMarkupProps> = function CheckboxM
|
||||
</checkbox-checkmark>
|
||||
<checkbox-mixedmark as='div' />
|
||||
</checkbox-background>
|
||||
<checkbox-ripple as='div' />
|
||||
{ripple && (
|
||||
<checkbox-ripple as='div' />
|
||||
)}
|
||||
</checkbox>
|
||||
{label && rest.id && <checkbox-label as='label' htmlFor={rest.id}>{label}</checkbox-label>}
|
||||
</checkbox-container>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({
|
||||
{...rest}
|
||||
checked={checkedControlled}
|
||||
/>
|
||||
<field-label htmlFor={rest.value || rest.name} as="label">{children}</field-label>
|
||||
<field-label htmlFor={rest.value || rest.name} title={rest.title} as="label">{children}</field-label>
|
||||
</field>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,24 +17,24 @@ import { RadioGroupContext } from './RadioGroupContext';
|
||||
|
||||
const radioStyles = composes(
|
||||
css`
|
||||
radio {
|
||||
composes: theme-radio from global;
|
||||
}
|
||||
radio-background {
|
||||
composes: theme-radio_background from global;
|
||||
}
|
||||
input {
|
||||
composes: theme-radio_native-control from global;
|
||||
}
|
||||
radio-outer-circle {
|
||||
composes: theme-radio_outer-circle from global;
|
||||
}
|
||||
radio-inner-circle {
|
||||
composes: theme-radio_inner-circle from global;
|
||||
}
|
||||
radio-ripple {
|
||||
composes: theme-radio_ripple from global;
|
||||
}
|
||||
radio {
|
||||
composes: theme-radio from global;
|
||||
}
|
||||
radio-background {
|
||||
composes: theme-radio_background from global;
|
||||
}
|
||||
input {
|
||||
composes: theme-radio_native-control from global;
|
||||
}
|
||||
radio-outer-circle {
|
||||
composes: theme-radio_outer-circle from global;
|
||||
}
|
||||
radio-inner-circle {
|
||||
composes: theme-radio_inner-circle from global;
|
||||
}
|
||||
radio-ripple {
|
||||
composes: theme-radio_ripple from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
field {
|
||||
@@ -55,10 +55,41 @@ const radioMod = {
|
||||
}
|
||||
`
|
||||
),
|
||||
small: composes(
|
||||
css`
|
||||
radio {
|
||||
composes: theme-radio_small from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
field {
|
||||
& radio {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
& radio-background {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
& radio-inner-circle {
|
||||
border-width: 7px;
|
||||
}
|
||||
}
|
||||
`
|
||||
),
|
||||
};
|
||||
|
||||
const noRippleStyles = composes(
|
||||
css`
|
||||
radio {
|
||||
composes: theme-radio_no-ripple from global;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'checked'> & {
|
||||
mod?: Array<keyof typeof radioMod>;
|
||||
ripple?: boolean;
|
||||
};
|
||||
|
||||
type ControlledProps = BaseProps & {
|
||||
@@ -91,6 +122,7 @@ export const Radio: RadioType = observer(function Radio({
|
||||
checked: controlledChecked,
|
||||
onChange,
|
||||
mod,
|
||||
ripple = true,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
@@ -131,7 +163,7 @@ export const Radio: RadioType = observer(function Radio({
|
||||
checked = state[name] === value;
|
||||
}
|
||||
|
||||
return styled(useStyles(radioStyles, ...(mod || []).map(mod => radioMod[mod])))(
|
||||
return styled(useStyles(radioStyles, ...(mod || []).map(mod => radioMod[mod]), !ripple && noRippleStyles))(
|
||||
<field as="div" className={className}>
|
||||
<radio as="div">
|
||||
<input
|
||||
@@ -147,7 +179,7 @@ export const Radio: RadioType = observer(function Radio({
|
||||
<radio-outer-circle as="div" />
|
||||
<radio-inner-circle as="div" />
|
||||
</radio-background>
|
||||
<radio-ripple as="div" />
|
||||
{ripple && <radio-ripple as="div" />}
|
||||
</radio>
|
||||
<label htmlFor={id}>{children}</label>
|
||||
</field>
|
||||
|
||||
@@ -85,7 +85,6 @@ const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
|
||||
checkboxLabel={property.displayName}
|
||||
title={property.description}
|
||||
disabled={disabled}
|
||||
mod='surface'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,7 +212,6 @@ export const Options: TabContainerPanelComponent<IConnectionFormTabProps> = obse
|
||||
disabled={edit || form.form.disabled}
|
||||
readOnly={form.form.readonly}
|
||||
// autoHide={} // maybe better to use autoHide
|
||||
mod='surface'
|
||||
>{translate('connections_connection_template')}
|
||||
</FieldCheckboxNew>
|
||||
)}
|
||||
@@ -245,7 +244,6 @@ export const Options: TabContainerPanelComponent<IConnectionFormTabProps> = obse
|
||||
value={data.config.connectionId + 'authNeeded'}
|
||||
state={data.config}
|
||||
disabled={form.form.disabled || form.form.readonly}
|
||||
mod='surface'
|
||||
gridItemMax
|
||||
>{translate('connections_connection_edit_save_credentials')}
|
||||
</FieldCheckboxNew>
|
||||
|
||||
@@ -12,7 +12,7 @@ import styled from 'reshadow';
|
||||
import { css } from 'reshadow';
|
||||
|
||||
import { useAdministrationSettings } from '@cloudbeaver/core-administration';
|
||||
import { Group, SubmittingForm, useMapResource, Button, ColoredContainer, InputFieldNew, FieldCheckboxNew, BASE_CONTAINERS_STYLES, SwitchNew, Grid, GroupItem, Container } from '@cloudbeaver/core-blocks';
|
||||
import { Group, SubmittingForm, useMapResource, Button, ColoredContainer, InputFieldNew, FieldCheckboxNew, BASE_CONTAINERS_STYLES, SwitchNew, GroupItem, Container } from '@cloudbeaver/core-blocks';
|
||||
import type { TabContainerPanelComponent } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
@@ -160,7 +160,6 @@ export const SSH: TabContainerPanelComponent<IConnectionFormTabProps> = observer
|
||||
value={SSH_TUNNEL_ID + ' savePassword'}
|
||||
state={state}
|
||||
disabled={disabled || !enabled || form.form.readonly}
|
||||
mod='surface'
|
||||
>{translate('connections_network_handler_ssh_tunnel_save_password')}
|
||||
</FieldCheckboxNew>
|
||||
)}
|
||||
|
||||
@@ -23,15 +23,26 @@ import {
|
||||
ResourceKey,
|
||||
ResourceKeyUtils,
|
||||
TestConnectionMutation,
|
||||
NavigatorSettingsInput,
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { ConnectionsResource, DatabaseConnection } from './Administration/ConnectionsResource';
|
||||
import { CONNECTION_NAVIGATOR_VIEW_SETTINGS } from './ConnectionNavigatorViewSettings';
|
||||
import type { NavigatorViewSettings } from './ConnectionNavigatorViewSettings';
|
||||
|
||||
export type Connection = DatabaseConnection & { authProperties?: UserConnectionAuthPropertiesFragment[] };
|
||||
export type ConnectionInitConfig = Omit<InitConnectionMutationVariables, 'includeOrigin' | 'customIncludeOriginDetails' | 'includeAuthProperties' | 'customIncludeNetworkHandlerCredentials'>;
|
||||
export type ConnectionInfoIncludes = Omit<GetUserConnectionsQueryVariables, 'id'>;
|
||||
|
||||
export const DEFAULT_NAVIGATOR_VIEW_SETTINGS: NavigatorSettingsInput = {
|
||||
showOnlyEntities: false,
|
||||
hideFolders: false,
|
||||
hideVirtualModel: false,
|
||||
hideSchemas: false,
|
||||
mergeEntities: false,
|
||||
showSystemObjects: false,
|
||||
showUtilityObjects: false,
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class ConnectionInfoResource extends CachedMapResource<string, Connection, ConnectionInfoIncludes> {
|
||||
readonly onConnectionCreate: IExecutor<Connection>;
|
||||
@@ -170,13 +181,12 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
|
||||
return this.get(config.id)!;
|
||||
}
|
||||
|
||||
async changeConnectionView(id: string, simple: boolean): Promise<Connection> {
|
||||
async changeConnectionView(id: string, settings: NavigatorViewSettings): Promise<Connection> {
|
||||
await this.performUpdate(id, [], async () => {
|
||||
const settings = simple ? CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple : CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced;
|
||||
|
||||
const connectionNavigatorViewSettings = this.get(id)?.navigatorSettings || DEFAULT_NAVIGATOR_VIEW_SETTINGS;
|
||||
const { connection } = await this.graphQLService.sdk.setConnectionNavigatorSettings({
|
||||
id,
|
||||
settings,
|
||||
settings: { ...connectionNavigatorViewSettings, ...settings },
|
||||
...this.getDefaultIncludes(),
|
||||
...this.getIncludesMap(id),
|
||||
});
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
|
||||
import type { NavigatorSettingsInput } from '@cloudbeaver/core-sdk';
|
||||
|
||||
type NavigatorView = 'simple' | 'advanced';
|
||||
export type NavigatorView = 'simple' | 'advanced';
|
||||
export type NavigatorViewSettings = Partial<NavigatorSettingsInput>;
|
||||
|
||||
export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record<NavigatorView, NavigatorSettingsInput> = {
|
||||
export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record<NavigatorView, NavigatorViewSettings> = {
|
||||
simple: {
|
||||
showOnlyEntities: true,
|
||||
hideFolders: true,
|
||||
hideVirtualModel: true,
|
||||
hideSchemas: false,
|
||||
mergeEntities: false,
|
||||
showSystemObjects: false,
|
||||
showUtilityObjects: false,
|
||||
},
|
||||
advanced: {
|
||||
@@ -26,13 +26,13 @@ export const CONNECTION_NAVIGATOR_VIEW_SETTINGS: Record<NavigatorView, Navigator
|
||||
hideVirtualModel: false,
|
||||
hideSchemas: false,
|
||||
mergeEntities: false,
|
||||
showSystemObjects: false,
|
||||
showUtilityObjects: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function isSimpleNavigatorView(settings: NavigatorSettingsInput): boolean {
|
||||
const simple = CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple;
|
||||
return !Object.keys(simple)
|
||||
.some(key => settings[key as keyof NavigatorSettingsInput] !== simple[key as keyof NavigatorSettingsInput]);
|
||||
export function isNavigatorViewSettingsEqual(
|
||||
settings: NavigatorSettingsInput, settingsToCompare: NavigatorViewSettings
|
||||
): boolean {
|
||||
return !(Object.keys(settingsToCompare) as Array<keyof NavigatorSettingsInput>)
|
||||
.some(key => settings[key] !== settingsToCompare[key]);
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({
|
||||
checkboxLabel={translate('connections_connection_edit_save_credentials')}
|
||||
disabled={controller.isAuthenticating}
|
||||
state={controller.config}
|
||||
mod='surface'
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
@@ -81,7 +81,6 @@ export const SSHAuthForm: React.FC<Props> = observer(function SSHAuthForm({
|
||||
state={state}
|
||||
checkboxLabel={translate('connections_network_handler_ssh_tunnel_save_password')}
|
||||
disabled={disabled}
|
||||
mod='surface'
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IMenuPanel } from '../IMenuPanel';
|
||||
import { ComputedMenuItemModel, IComputedMenuItemOptions } from '../models/ComputedMenuItemModel';
|
||||
import { MenuOptionsStore } from '../models/MenuOptionsStore';
|
||||
import { MenuItemType, MenuOptionsStore } from '../models/MenuOptionsStore';
|
||||
import type { IContextMenuItem } from './IContextMenuItem';
|
||||
import type { IMenuContext } from './IMenuContext';
|
||||
|
||||
@@ -89,6 +89,9 @@ class ComputedMenuItemOptionsWithContext<T> implements IComputedMenuItemOptions
|
||||
isHidden?: () => boolean;
|
||||
// set icon or getter
|
||||
icon?: string;
|
||||
isChecked?: () => boolean;
|
||||
type?: MenuItemType;
|
||||
separator?: boolean;
|
||||
iconGetter?: () => string | undefined;
|
||||
|
||||
constructor(private options: IContextMenuItem<T>,
|
||||
@@ -97,6 +100,8 @@ class ComputedMenuItemOptionsWithContext<T> implements IComputedMenuItemOptions
|
||||
this.title = options.title;
|
||||
this.titleGetter = options.titleGetter;
|
||||
this.icon = options.icon;
|
||||
this.type = options.type;
|
||||
this.separator = options.separator;
|
||||
this.iconGetter = options.iconGetter;
|
||||
|
||||
this.id = `${options.id}-${context.contextId!}`;
|
||||
@@ -110,6 +115,9 @@ class ComputedMenuItemOptionsWithContext<T> implements IComputedMenuItemOptions
|
||||
if (options.isHidden) {
|
||||
this.isHidden = () => options.isHidden!(this.context);
|
||||
}
|
||||
if (options.isChecked) {
|
||||
this.isChecked = () => options.isChecked!(this.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,4 +19,5 @@ export interface IContextMenuItem<T> extends IMenuItemOptions {
|
||||
isDisabled?: (context: IMenuContext<T>) => boolean;
|
||||
// When the item is present in menu it can be hidden based on certain conditions
|
||||
isHidden?: (context: IMenuContext<T>) => boolean;
|
||||
isChecked?: (context: IMenuContext<T>) => boolean;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { MenuInitialState } from 'reakit/Menu';
|
||||
import type { TLocalizationToken } from '@cloudbeaver/core-localization';
|
||||
import type { Style } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { MenuItemType } from './models/MenuOptionsStore';
|
||||
|
||||
export type MenuMod = 'primary' | 'surface' | 'secondary';
|
||||
|
||||
export interface IMenuPanel {
|
||||
@@ -28,6 +30,9 @@ export interface IMenuItem {
|
||||
isHidden?: boolean;
|
||||
icon?: string; // path to icon or svg icon name
|
||||
panel?: IMenuPanel; // if menu has sub-items
|
||||
type?: MenuItemType;
|
||||
separator?: boolean;
|
||||
isChecked?: boolean;
|
||||
}
|
||||
|
||||
export type MenuTriggerProps = PropsWithChildren<{
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { Icon } from '@cloudbeaver/core-blocks';
|
||||
import { Checkbox, Icon, IconOrImage, Radio } from '@cloudbeaver/core-blocks';
|
||||
import type { IMenuItem } from '@cloudbeaver/core-dialogs';
|
||||
import { Translate } from '@cloudbeaver/core-localization';
|
||||
import { useStyles, Style } from '@cloudbeaver/core-theming';
|
||||
import { use } from '@reshadow/react';
|
||||
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
|
||||
@@ -25,17 +26,37 @@ export const MenuPanelItem: React.FC<MenuPanelItemProps> = function MenuPanelIte
|
||||
menuItem,
|
||||
style = [],
|
||||
}) {
|
||||
const { title, panel } = useObserver(() => ({ // TODO: provide title and panel via props
|
||||
const { title, panel, icon, isChecked, controlType, separator } = useObserver(() => ({ // TODO: provide title and panel via props
|
||||
title: menuItem.title,
|
||||
panel: menuItem.panel,
|
||||
icon: menuItem.icon,
|
||||
isChecked: menuItem.isChecked,
|
||||
controlType: menuItem.type,
|
||||
separator: menuItem.separator,
|
||||
}));
|
||||
|
||||
let control = null;
|
||||
|
||||
if (controlType === 'radio') {
|
||||
control = <Radio checked={isChecked} mod={['primary', 'small']} ripple={false} />;
|
||||
} else if (controlType === 'checkbox') {
|
||||
control = <Checkbox checked={isChecked} mod={['primary', 'small']} ripple={false} />;
|
||||
}
|
||||
|
||||
return styled(useStyles(menuPanelStyles, ...style))(
|
||||
<menu-panel-item as="div">
|
||||
<menu-item-text as="span">
|
||||
<menu-panel-item as="div" {...use({ separator })}>
|
||||
<menu-item-content as='div'>
|
||||
{icon ? (
|
||||
<IconOrImage icon={icon} />
|
||||
) : control}
|
||||
</menu-item-content>
|
||||
<menu-item-text as="div">
|
||||
<Translate token={title} />
|
||||
</menu-item-text>
|
||||
{panel && <Icon name="arrow" viewBox="0 0 16 16" />}
|
||||
<menu-item-content as='div'>
|
||||
{panel && <Icon name="arrow" viewBox="0 0 16 16" />}
|
||||
</menu-item-content>
|
||||
</menu-panel-item>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'react';
|
||||
import {
|
||||
MenuButton,
|
||||
Menu, MenuItem, MenuStateReturn, useMenuState
|
||||
Menu, MenuItem, MenuStateReturn, useMenuState, MenuItemCheckbox, MenuItemRadio
|
||||
} from 'reakit/Menu';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
@@ -117,6 +117,42 @@ const MenuPanelElement = observer(function MenuPanelElement({
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === 'radio') {
|
||||
return styled(styles)(
|
||||
<MenuItemRadio
|
||||
{...menu}
|
||||
{...use({ hidden: item.isHidden })}
|
||||
aria-label={item.id}
|
||||
disabled={item.isDisabled}
|
||||
name={item.id}
|
||||
value={item.title}
|
||||
checked={item.isChecked}
|
||||
onClick={onClick}
|
||||
>
|
||||
<MenuPanelItem menuItem={item} style={style} />
|
||||
</MenuItemRadio>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === 'checkbox') {
|
||||
return styled(styles)(
|
||||
<MenuItemCheckbox
|
||||
{...menu}
|
||||
{...use({ hidden: item.isHidden })}
|
||||
aria-label={item.id}
|
||||
disabled={item.isDisabled}
|
||||
name={item.id}
|
||||
value={item.title}
|
||||
checked={item.isChecked}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
<MenuPanelItem menuItem={item} style={style} />
|
||||
</MenuItemCheckbox>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<MenuItem
|
||||
{...menu}
|
||||
|
||||
@@ -16,12 +16,17 @@ export const menuPanelStyles = composes(
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
}
|
||||
MenuItem,
|
||||
MenuItemCheckbox,
|
||||
MenuItemRadio,
|
||||
MenuButton {
|
||||
composes: theme-ripple from global;
|
||||
}
|
||||
MenuItemElement {
|
||||
composes: theme-ripple from global;
|
||||
}
|
||||
menu-panel-item {
|
||||
composes: theme-border-color-background from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
MenuButton {
|
||||
@@ -30,8 +35,6 @@ export const menuPanelStyles = composes(
|
||||
outline: none !important;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 12px 0;
|
||||
|
||||
& box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -52,11 +55,12 @@ export const menuPanelStyles = composes(
|
||||
margin-top: -12px;
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
MenuItem, MenuItemCheckbox, MenuItemRadio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
padding: 6px 16px;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
@@ -69,26 +73,42 @@ export const menuPanelStyles = composes(
|
||||
|
||||
&:hover, &:global([aria-expanded="true"]) {
|
||||
font-weight: 600;
|
||||
|
||||
& Icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu-panel-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 30px;
|
||||
padding: 0 4px;
|
||||
&[|separator] {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
& menu-item-text {
|
||||
display: block;
|
||||
padding: 0 4px;
|
||||
flex: 1;
|
||||
}
|
||||
& menu-item-content {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
& Icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.5;
|
||||
padding-left: 16px;
|
||||
}
|
||||
& IconOrImage {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -11,18 +11,21 @@ import { computed, makeObservable } from 'mobx';
|
||||
import type { IMenuItem, IMenuPanel } from '@cloudbeaver/core-dialogs';
|
||||
import type { TLocalizationToken } from '@cloudbeaver/core-localization';
|
||||
|
||||
import type { IMenuItemOptions } from './MenuOptionsStore';
|
||||
import type { IMenuItemOptions, MenuItemType } from './MenuOptionsStore';
|
||||
|
||||
export interface IComputedMenuItemOptions extends IMenuItemOptions {
|
||||
onClick?: () => void;
|
||||
isDisabled?: () => boolean;
|
||||
isHidden?: () => boolean;
|
||||
isChecked?: () => boolean;
|
||||
}
|
||||
|
||||
export class ComputedMenuItemModel implements IMenuItem {
|
||||
id: string;
|
||||
onClick?: () => void;
|
||||
panel?: IMenuPanel;
|
||||
type?: MenuItemType;
|
||||
separator?: boolean;
|
||||
rtl?: boolean;
|
||||
|
||||
get title(): TLocalizationToken {
|
||||
@@ -47,15 +50,22 @@ export class ComputedMenuItemModel implements IMenuItem {
|
||||
return this.options.isHidden ? this.options.isHidden() : false;
|
||||
}
|
||||
|
||||
get isChecked() {
|
||||
return this.options.isChecked ? this.options.isChecked() : false;
|
||||
}
|
||||
|
||||
constructor(private options: IComputedMenuItemOptions) {
|
||||
makeObservable(this, {
|
||||
title: computed,
|
||||
isDisabled: computed,
|
||||
icon: computed,
|
||||
isHidden: computed,
|
||||
isChecked: computed,
|
||||
});
|
||||
|
||||
this.id = options.id;
|
||||
this.type = options.type;
|
||||
this.separator = options.separator;
|
||||
this.rtl = options.rtl;
|
||||
this.panel = options.panel;
|
||||
this.onClick = this.options.onClick;
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { IMenuPanel } from '../IMenuPanel';
|
||||
|
||||
const DEFAULT_ITEM_ORDER = 100;
|
||||
|
||||
export type MenuItemType = 'checkbox' | 'radio';
|
||||
export interface IMenuItemOptions {
|
||||
id: string;
|
||||
// set title or getter
|
||||
@@ -24,6 +25,8 @@ export interface IMenuItemOptions {
|
||||
order?: number;
|
||||
isPanel?: boolean;
|
||||
panel?: IMenuPanel;
|
||||
type?: MenuItemType;
|
||||
separator?: boolean;
|
||||
rtl?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,44 +6,53 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
@import '@material/checkbox/mixins';
|
||||
$mdc-checkbox-icon-size: 16px;
|
||||
@import "@material/checkbox/mixins";
|
||||
$mdc-checkbox-icon-size: 16px;
|
||||
|
||||
@mixin checkbox-core-styles($query: mdc-feature-all()) {
|
||||
@include mdc-checkbox-core-styles($query);
|
||||
|
||||
.theme-checkbox {
|
||||
composes: mdc-checkbox;
|
||||
}
|
||||
.theme-checkbox_native-control {
|
||||
composes: mdc-checkbox__native-control;
|
||||
}
|
||||
.theme-checkbox__background {
|
||||
composes: mdc-checkbox__background;
|
||||
}
|
||||
.theme-checkbox__checkmark {
|
||||
composes: mdc-checkbox__checkmark;
|
||||
}
|
||||
.theme-checkbox__checkmark-path {
|
||||
composes: mdc-checkbox__checkmark-path;
|
||||
}
|
||||
.theme-checkbox__mixedmark {
|
||||
@mixin checkbox-core-styles($query: mdc-feature-all()) {
|
||||
@include mdc-checkbox-core-styles($query);
|
||||
|
||||
.theme-checkbox {
|
||||
composes: mdc-checkbox;
|
||||
}
|
||||
.theme-checkbox_native-control {
|
||||
composes: mdc-checkbox__native-control;
|
||||
}
|
||||
.theme-checkbox__background {
|
||||
composes: mdc-checkbox__background;
|
||||
}
|
||||
.theme-checkbox__checkmark {
|
||||
composes: mdc-checkbox__checkmark;
|
||||
}
|
||||
.theme-checkbox__checkmark-path {
|
||||
composes: mdc-checkbox__checkmark-path;
|
||||
}
|
||||
.theme-checkbox__mixedmark {
|
||||
composes: mdc-checkbox__mixedmark;
|
||||
}
|
||||
.theme-checkbox__ripple {
|
||||
}
|
||||
.theme-checkbox__ripple {
|
||||
composes: mdc-checkbox__ripple;
|
||||
}
|
||||
.theme-checkbox--checked {
|
||||
composes: mdc-checkbox--selected;
|
||||
}
|
||||
.theme-checkbox--disabled {
|
||||
composes: mdc-checkbox--disabled;
|
||||
}
|
||||
|
||||
.theme-checkbox_primary {
|
||||
$mdc-checkbox-mark-color: $mdc-theme-on-primary;
|
||||
@include mdc-checkbox-ink-color($mdc-checkbox-mark-color, $query);
|
||||
@include mdc-checkbox-disabled-ink-color(rgba($mdc-checkbox-mark-color, .50), $query);
|
||||
@include mdc-checkbox-container-colors($mdc-checkbox-border-color, transparent, $mdc-theme-primary, $mdc-theme-primary )
|
||||
}
|
||||
}
|
||||
}
|
||||
.theme-checkbox--checked {
|
||||
composes: mdc-checkbox--selected;
|
||||
}
|
||||
.theme-checkbox--disabled {
|
||||
composes: mdc-checkbox--disabled;
|
||||
}
|
||||
|
||||
.theme-checkbox_primary {
|
||||
$mdc-checkbox-mark-color: $mdc-theme-on-primary;
|
||||
@include mdc-checkbox-ink-color($mdc-checkbox-mark-color, $query);
|
||||
@include mdc-checkbox-disabled-ink-color(rgba($mdc-checkbox-mark-color, 0.5), $query);
|
||||
@include mdc-checkbox-container-colors(
|
||||
$mdc-checkbox-border-color,
|
||||
transparent,
|
||||
$mdc-theme-primary,
|
||||
$mdc-theme-primary
|
||||
);
|
||||
}
|
||||
|
||||
.theme-checkbox_small {
|
||||
@include mdc-checkbox-density(-3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
.theme-border-color-#{$style} {
|
||||
@include mdc-feature-targets($feat-color) {
|
||||
@include mdc-theme-prop(border-color, $style);
|
||||
@include mdc-theme-prop(border-color, $style, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
@import '@material/radio/mixins';
|
||||
@import "@material/radio/mixins";
|
||||
|
||||
@mixin radio-core-styles($query: mdc-feature-all()) {
|
||||
@include mdc-radio-core-styles;
|
||||
|
||||
|
||||
.theme-radio {
|
||||
composes: mdc-radio;
|
||||
}
|
||||
@@ -32,8 +32,8 @@
|
||||
|
||||
.theme-radio_primary {
|
||||
$mdc-radio-baseline-theme-color: primary;
|
||||
$mdc-radio-unchecked-color: rgba(mdc-theme-prop-value(on-surface), .54);
|
||||
$mdc-radio-disabled-circle-color: rgba(mdc-theme-prop-value(on-surface), .26);
|
||||
$mdc-radio-unchecked-color: rgba(mdc-theme-prop-value(on-surface), 0.54);
|
||||
$mdc-radio-disabled-circle-color: rgba(mdc-theme-prop-value(on-surface), 0.26);
|
||||
|
||||
@include mdc-radio-unchecked-stroke-color($mdc-radio-unchecked-color, $query: $query);
|
||||
@include mdc-radio-checked-stroke-color($mdc-radio-baseline-theme-color, $query: $query);
|
||||
@@ -43,4 +43,12 @@
|
||||
@include mdc-radio-disabled-ink-color($mdc-radio-disabled-circle-color, $query: $query);
|
||||
@include mdc-radio-focus-indicator-color($mdc-radio-baseline-theme-color, $query: $query);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-radio_small {
|
||||
@include mdc-radio-density(-3);
|
||||
}
|
||||
|
||||
.theme-radio_no-ripple {
|
||||
@include mdc-radio-ripple-size(0);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { FormGroup, Switch } from '@cloudbeaver/core-blocks';
|
||||
import { isSimpleNavigatorView, CONNECTION_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections';
|
||||
import { CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual } from '@cloudbeaver/core-connections';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
|
||||
import type { IServerConfigurationPageState } from '../IServerConfigurationPageState';
|
||||
@@ -24,13 +24,13 @@ export const ServerConfigurationNavigatorViewForm: React.FC<Props> = observer(fu
|
||||
}) {
|
||||
const translate = useTranslate();
|
||||
|
||||
const isSimpleView = isSimpleNavigatorView(configs.navigatorConfig);
|
||||
const isSimpleView = isNavigatorViewSettingsEqual(configs.navigatorConfig, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple);
|
||||
|
||||
const onNavigatorViewChangeHandler = useCallback((value: boolean) => {
|
||||
if (value) {
|
||||
configs.navigatorConfig = { ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple };
|
||||
configs.navigatorConfig = { ...configs.navigatorConfig, ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple };
|
||||
} else {
|
||||
configs.navigatorConfig = { ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced };
|
||||
configs.navigatorConfig = { ...configs.navigatorConfig, ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced };
|
||||
}
|
||||
}, [configs]);
|
||||
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import { observable, makeObservable } from 'mobx';
|
||||
|
||||
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import { UsersResource } from '@cloudbeaver/core-authentication';
|
||||
import { CONNECTION_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections';
|
||||
import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { IExecutor, Executor } from '@cloudbeaver/core-executor';
|
||||
@@ -152,7 +152,7 @@ export class ServerConfigurationService {
|
||||
customConnectionsEnabled: true,
|
||||
sessionExpireTime: 30,
|
||||
},
|
||||
navigatorConfig: { ...CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced },
|
||||
navigatorConfig: { ...DEFAULT_NAVIGATOR_VIEW_SETTINGS },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,6 @@ export const UserInfo: TabContainerPanelComponent<IUserFormProps> = observer(fun
|
||||
checkboxLabel={role.roleName || role.roleId}
|
||||
checked={!!controller.credentials.roles.get(role.roleId)}
|
||||
disabled={controller.isSaving}
|
||||
mod='surface'
|
||||
onChange={checked => handleRoleChange(role.roleId, checked)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -115,7 +115,6 @@ export const ConnectionDialog = observer(function ConnectionDialog({
|
||||
checkboxLabel={translate('connections_connection_edit_save_credentials')}
|
||||
disabled={controller.isConnecting}
|
||||
state={controller.config}
|
||||
mod='surface'
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user