CB-3456 refactor: user form (#2010)

* CB-3456 chore: cleanup users administration

* CB-3456 chore: add label for user credentials column

* CB-3456 chore: add deprecation message to reshadow system

* CB-3456 chore: add deprecation message to ComponentStyle

* CB-3456 refactor: user form

* CB-3456 fix: eslint problems

* CB-3456 fix: after review

* CB-3456 fix: base form localization

* CB-3456 fix: remove unused import

* CB-3456 fix: user default team assignment

* CB-3456 chore: update git indexes

* CB-3456 fix: imports

* CB-3456 fix: enable user by default

* CB-3456 refactor: implement FormPart

* CB-3456 fix: make sh executable

* CB-3456 fix: make sh executable

* CB-3456 fix: handle form part exceptions on save

---------

Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
Alexey
2023-09-28 16:28:28 +03:00
committed by GitHub
co-authored by EvgeniaBzzz
parent c58e62c90e
commit 0d41713c2d
199 changed files with 3039 additions and 2561 deletions
+6 -1
View File
@@ -36,7 +36,12 @@
{
"label": "Build CE",
"type": "shell",
"command": "./build-sqlite.bat",
"windows": {
"command": "./build-sqlite.bat"
},
"osx": {
"command": "./build-sqlite.sh"
},
"options": {
"cwd": "${workspaceFolder}/deploy"
},
Regular → Executable
View File
@@ -117,6 +117,7 @@ type ServerConfig {
hostName: String! @deprecated # use container id instead
containerId: String!
defaultAuthRole: String
defaultUserTeam: String # [23.2.2]
anonymousAccessEnabled: Boolean!
supportsCustomConnections: Boolean!
@@ -212,4 +212,9 @@ public class WebServerConfig {
public String getDefaultAuthRole() {
return application.getDefaultAuthRole();
}
@Property
public String getDefaultUserTeam() {
return application.getAppConfiguration().getDefaultUserTeam();
}
}
@@ -56,12 +56,6 @@ export const UsersResourceNewUsers = resourceKeyListAlias('@users-resource/new-u
interface UserCreateOptions {
userId: string;
teams: string[];
credentials: IAuthCredentials;
metaParameters: Record<string, any>;
grantedConnections: string[];
enabled: boolean;
authRole?: string;
}
@injectable()
@@ -129,30 +123,17 @@ export class UsersResource extends CachedMapResource<string, AdminUser, UserReso
await this.graphQLService.sdk.saveUserMetaParameters({ userId, parameters });
}
async create({ userId, teams, credentials, metaParameters, grantedConnections, enabled, authRole }: UserCreateOptions): Promise<AdminUser> {
async create({ userId }: UserCreateOptions): Promise<AdminUser> {
const { user } = await this.graphQLService.sdk.createUser({
userId,
enabled,
authRole,
enabled: false,
...this.getDefaultIncludes(),
...this.getIncludesMap(userId),
});
try {
await this.updateCredentials(userId, credentials);
for (const teamId of teams) {
await this.grantTeam(userId, teamId, true);
}
await this.setConnections(userId, grantedConnections);
await this.setMetaParameters(userId, metaParameters);
const user = (await this.refresh(userId)) as unknown as AdminUserNew;
user[NEW_USER_SYMBOL] = true;
} catch (exception: any) {
this.delete(userId);
throw exception;
}
const newUser = user as unknown as AdminUserNew;
newUser[NEW_USER_SYMBOL] = true;
this.set(user.userId, newUser);
return this.get(user.userId)!;
}
@@ -31,4 +31,8 @@
&.center {
margin: 0 auto;
}
&.overflow {
overflow: auto;
}
}
@@ -0,0 +1,67 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 React, { forwardRef, useState } from 'react';
import { useCombinedRef } from '../useCombinedRef';
import { useFocus } from '../useFocus';
import { FormChangeHandler, FormContext, IFormContext } from './FormContext';
import { useForm } from './useForm';
type FormDetailedProps = Omit<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, 'onChange' | 'onSubmit'> & {
context?: IFormContext;
disabled?: boolean;
disableEnterSubmit?: boolean;
focusFirstChild?: boolean;
onSubmit?: (event?: SubmitEvent) => Promise<void> | void;
onChange?: FormChangeHandler;
};
export const Form = forwardRef<HTMLFormElement, FormDetailedProps>(function Form(
{ context, disabled: disabledProp, disableEnterSubmit, focusFirstChild, children, onSubmit, onChange, ...rest },
ref,
) {
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild });
const [disabledLocal, setDisabledLocal] = useState(false);
const disabled = disabledLocal || disabledProp || false;
const formContext = useForm({
disableEnterSubmit,
parent: context,
onSubmit(event) {
const result = onSubmit?.(event);
if (result instanceof Promise) {
setDisabledLocal(true);
result.finally(() => {
setDisabledLocal(false);
});
}
},
onChange,
});
const setFormRef = useCombinedRef<HTMLFormElement>(formContext.setRef, focusedRef, ref);
if (formContext.parent && formContext.parent !== context) {
return (
<fieldset disabled={disabled} className={rest.className}>
<FormContext.Provider value={formContext}>{children}</FormContext.Provider>
</fieldset>
);
}
return (
<form {...rest} ref={setFormRef}>
<fieldset disabled={disabled} className={rest.className}>
<FormContext.Provider value={formContext}>{children}</FormContext.Provider>
</fieldset>
<button type="submit" disabled={disableEnterSubmit} aria-hidden={disableEnterSubmit} hidden />
</form>
);
});
@@ -7,7 +7,7 @@
*/
import { createContext } from 'react';
import type { IExecutor } from '@cloudbeaver/core-executor';
import type { IExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
export type FormChangeValues = string | number | boolean | FileList | null | undefined;
export type FormChangeHandler = (value: FormChangeValues, name: string | undefined) => void;
@@ -19,9 +19,18 @@ export interface IChangeData {
}
export interface IFormContext {
changeExecutor: IExecutor<IChangeData>;
ref: HTMLFormElement | null;
onValidate: SyncExecutor;
onSubmit: SyncExecutor;
onChange: IExecutor<IChangeData>;
parent: IFormContext | null;
disableEnterSubmit: boolean;
setRef: (ref: HTMLFormElement | null) => void;
change: FormChangeHandler;
keyDown: KeyHandler;
validate: () => boolean;
reportValidity: () => boolean;
submit: (event?: SubmitEvent) => void;
}
export const FormContext = createContext<IFormContext | null>(null);
@@ -20,6 +20,7 @@ import { Loader } from '../Loader/Loader';
import { useTranslate } from '../localization/useTranslate';
import { s } from '../s';
import { useCombinedHandler } from '../useCombinedHandler';
import { useCombinedRef } from '../useCombinedRef';
import { useMergeRefs } from '../useMergeRefs';
import { useS } from '../useS';
import { useStateDelay } from '../useStateDelay';
@@ -97,7 +98,7 @@ export const InputField: InputFieldType = observer(
ref,
) {
const inputRef = useRef<HTMLInputElement | null>(null);
const mergedRef = useMergeRefs(inputRef, ref);
const mergedRef = useCombinedRef(inputRef, ref);
const capsLock = useCapsLockTracker();
const [passwordRevealed, setPasswordRevealed] = useState(false);
const translate = useTranslate();
@@ -137,9 +138,9 @@ export const InputField: InputFieldType = observer(
const handleKeyDown = useCombinedHandler(rest.onKeyDown, capsLock.handleKeyDown, context?.keyDown);
const passwordType = rest.type === 'password';
const uncontrolled = passwordType && !canShowPassword;
let uncontrolled = passwordType && !canShowPassword;
let value: any = valueControlled ?? defaultValue ?? undefined;
let value: any = valueControlled ?? undefined;
if (state && name !== undefined && name in state) {
value = state[name];
@@ -153,6 +154,8 @@ export const InputField: InputFieldType = observer(
description = translate('ui_capslock_on');
}
uncontrolled ||= value === undefined;
useLayoutEffect(() => {
if (uncontrolled && isNotNullDefined(value) && inputRef.current) {
inputRef.current.value = value;
@@ -176,6 +179,7 @@ export const InputField: InputFieldType = observer(
type={passwordRevealed ? 'text' : rest.type}
name={name}
value={uncontrolled ? undefined : value ?? ''}
defaultValue={defaultValue}
className={styles.input}
onChange={handleChange}
onBlur={handleBlur}
@@ -1,101 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 React, { forwardRef, useContext, useState } from 'react';
import { Executor } from '@cloudbeaver/core-executor';
import { useCombinedRef } from '../useCombinedRef';
import { useObjectRef } from '../useObjectRef';
import { FormChangeHandler, FormContext, IChangeData, IFormContext } from './FormContext';
export interface IFormStateInfo {
edited: boolean;
disabled: boolean;
readonly: boolean;
statusMessage: string | null;
}
type FormDetailedProps = Omit<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, 'onChange' | 'onSubmit'> & {
disabled?: boolean;
disableEnterSubmit?: boolean;
onSubmit?: (event?: React.FormEvent<HTMLFormElement>) => Promise<void> | void;
onChange?: FormChangeHandler;
};
export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(function SubmittingForm(
{ disabled: disabledProp, disableEnterSubmit, children, onSubmit, onChange = () => {}, ...rest },
ref,
) {
const [formRef, setFormInnerRef] = useState<HTMLFormElement | null>(null);
const setFormRef = useCombinedRef<HTMLFormElement>(setFormInnerRef, ref);
let [disabled, setDisabled] = useState(false);
disabled = disabled || disabledProp || false;
const props = useObjectRef(
() => ({
handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setDisabled(true);
const result = this.onSubmit?.(e);
if (result instanceof Promise) {
result.finally(() => {
setDisabled(false);
});
} else {
setDisabled(false);
}
},
}),
{
formRef,
disableEnterSubmit,
parentContext: useContext(FormContext),
onChange,
onSubmit,
},
['handleSubmit'],
);
const context = useObjectRef<IFormContext>(
() => ({
changeExecutor: new Executor<IChangeData>(),
change(value, name) {
props.onChange(value, name);
props.parentContext?.change(value, name);
this.changeExecutor.execute({ value, name });
},
keyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
const form = event.currentTarget.closest('form');
if (form) {
event.preventDefault();
const submitButton = form.querySelector<HTMLButtonElement>('button[type=submit]');
if (submitButton) {
submitButton.click();
}
}
}
props.parentContext?.keyDown(event);
},
}),
false,
['change', 'keyDown'],
);
return (
<form {...rest} ref={setFormRef} onSubmit={e => props.handleSubmit(e)}>
<fieldset disabled={disabled} className={rest.className}>
<FormContext.Provider value={context}>{children}</FormContext.Provider>
</fieldset>
<button type="submit" disabled={disableEnterSubmit} aria-hidden={disableEnterSubmit} hidden />
</form>
);
});
@@ -0,0 +1,63 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { useContext, useEffect, useRef } from 'react';
import { ExecutorInterrupter } from '@cloudbeaver/core-executor';
import { useExecutor } from '../useExecutor';
import { FormContext } from './FormContext';
export function useCustomInputValidation<T = void>(validation: (value: T) => string | null): React.RefObject<HTMLInputElement> {
const context = useContext(FormContext);
const inputRef = useRef<HTMLInputElement>(null);
useExecutor({
executor: context?.onValidate,
handlers: [
function validationHandler(_, context) {
if (!inputRef.current) {
return;
}
let value: T = undefined as unknown as T;
if (inputRef.current instanceof HTMLInputElement) {
value = inputRef.current.value as unknown as T;
}
const result = validation(value);
if (typeof result === 'string') {
inputRef.current.setCustomValidity(result);
ExecutorInterrupter.interrupt(context);
} else {
inputRef.current?.setCustomValidity('');
}
},
],
});
useEffect(() => {
const element = inputRef.current;
if (!element) {
return;
}
function resetValidationMessage() {
element?.setCustomValidity('');
}
element.addEventListener('input', resetValidationMessage);
return () => {
element?.removeEventListener('input', resetValidationMessage);
};
});
return inputRef;
}
@@ -0,0 +1,145 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 React, { useContext, useState } from 'react';
import { Executor, ExecutorInterrupter, SyncExecutor } from '@cloudbeaver/core-executor';
import { useExecutor } from '../useExecutor';
import { useObjectRef } from '../useObjectRef';
import { FormChangeHandler, FormContext, type IChangeData, type IFormContext } from './FormContext';
interface IOptions {
parent?: IFormContext;
disableEnterSubmit?: boolean;
onSubmit?: (event?: SubmitEvent | undefined) => void;
onChange?: FormChangeHandler;
}
export function useForm(options?: IOptions): IFormContext {
let parentForm = useContext(FormContext);
const [submittingExecutor] = useState(() => new SyncExecutor());
const [validationExecutor] = useState(() => new SyncExecutor());
const [changeExecutor] = useState(() => new Executor<IChangeData>());
if (options?.parent) {
parentForm = options.parent;
}
const disableEnterSubmit = options?.disableEnterSubmit ?? parentForm?.disableEnterSubmit ?? false;
useExecutor({
executor: parentForm?.onChange,
before: changeExecutor,
});
useExecutor({
executor: changeExecutor,
handlers: [({ value, name }) => options?.onChange?.(value, name)],
});
useExecutor({
executor: parentForm?.onSubmit,
before: submittingExecutor,
});
useExecutor({
executor: submittingExecutor,
handlers: [() => options?.onSubmit?.()],
});
const context = useObjectRef<IFormContext>(
() => ({
ref: null,
onValidate: validationExecutor,
onChange: changeExecutor,
onSubmit: submittingExecutor,
setRef(ref) {
if (this.parent) {
this.parent.setRef(ref);
} else {
if (this.ref) {
this.ref.removeEventListener('submit', this.submit);
}
this.ref = ref;
if (ref) {
ref.addEventListener('submit', this.submit);
}
}
},
change(value, name) {
if (this.parent) {
this.parent.change(value, name);
} else {
this.onChange.execute({ value, name });
}
},
keyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (this.parent) {
this.parent.keyDown(event);
} else if (event.key === 'Enter' && this.disableEnterSubmit === false) {
event.preventDefault();
this.submit();
}
},
submit(event) {
if (this.parent) {
this.parent.submit(event);
} else {
event?.preventDefault();
if (this.validate()) {
this.onSubmit.execute();
}
}
},
reportValidity() {
if (this.parent) {
return this.parent.reportValidity();
}
return this.ref?.reportValidity() ?? false;
},
validate() {
if (this.parent) {
return this.parent.validate();
} else {
const validationContext = this.onValidate.execute();
const interrupted = ExecutorInterrupter.isInterrupted(validationContext);
if (interrupted) {
context.ref?.reportValidity();
}
return interrupted === false;
}
},
}),
{ parent: parentForm, disableEnterSubmit },
['setRef', 'change', 'keyDown', 'submit', 'validate'],
);
useExecutor({
executor: parentForm?.onValidate,
before: validationExecutor,
});
useExecutor({
executor: validationExecutor,
handlers: [
function validateFormConstraints(_, executorContext) {
if (!context.ref) {
return;
}
if (context.ref.checkValidity() === false) {
ExecutorInterrupter.interrupt(executorContext);
}
},
],
});
return context;
}
@@ -7,7 +7,7 @@
*/
import { useEffect, useState } from 'react';
import { ILoadableState, isLoadableStateHasException } from '@cloudbeaver/core-utils';
import type { ILoadableState } from '@cloudbeaver/core-utils';
import { getComputed } from '../getComputed';
@@ -24,29 +24,35 @@ export function useAutoLoad(component: { name: string }, state: IAutoLoadable |
for (const loader of state as IAutoLoadable[]) {
getComputed(
// activate mobx subscriptions
() => (!loader.isLoaded() || loader.isOutdated?.() === true) && !isLoadableStateHasException(loader),
() => (!loader.isLoaded() || loader.isOutdated?.() === true) && !loader.isError(),
);
}
const obj = {
[loadFunctionName]: () => {
if (!enabled) {
return;
}
for (const loader of state as IAutoLoadable[]) {
if (loader.isError() || (loader.lazy === true && !lazy)) {
continue;
}
if (!loader.isLoaded() || loader.isOutdated?.() === true) {
loader.load();
}
}
},
};
const promises = state.map(loader => loader.promise).filter(Boolean) as Promise<any>[];
if (promises.length > 0) {
throw Promise.all(promises);
}
useEffect(() => {
const obj = {
[loadFunctionName]: () => {
if (!enabled) {
return;
}
for (const loader of state as IAutoLoadable[]) {
if (isLoadableStateHasException(loader) || (loader.lazy === true && !lazy)) {
continue;
}
if (!loader.isLoaded() || loader.isOutdated?.() === true) {
loader.load();
}
}
},
};
obj[loadFunctionName]();
});
}
@@ -172,15 +172,15 @@ export function useResource<
});
function getData(): any {
if (actions?.active === false) {
if (isResourceKeyList(propertiesRef.key) || isResourceKeyListAlias(propertiesRef.key)) {
return [];
}
return undefined;
if (propertiesRef.resource instanceof CachedDataResource) {
return propertiesRef.resource.data;
}
if (propertiesRef.resource instanceof CachedMapResource) {
if (propertiesRef.key === null) {
if (actions?.active === false || propertiesRef.key === null) {
if (isResourceKeyList(propertiesRef.key) || isResourceKeyListAlias(propertiesRef.key)) {
return [];
}
return undefined;
}
return propertiesRef.resource.get(propertiesRef.key);
@@ -0,0 +1,10 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
.block {
display: block;
}
@@ -7,19 +7,26 @@
*/
import { GlobalConstants, isValidUrl } from '@cloudbeaver/core-utils';
import { s } from './s';
import style from './StaticImage.m.css';
import { useS } from './useS';
interface Props {
icon?: string;
className?: string;
title?: string;
width?: number;
block?: boolean;
onClick?: (e: React.MouseEvent) => void;
}
export const StaticImage: React.FC<Props> = function StaticImage({ icon, className, title, onClick }) {
export const StaticImage: React.FC<Props> = function StaticImage({ icon, className, title, width, block, onClick }) {
const styles = useS(style);
if (!icon) {
return null;
}
const url = isValidUrl(icon) ? icon : GlobalConstants.absoluteUrl(icon);
return <img className={className} src={url} title={title} onClick={onClick} />;
return <img className={s(styles, { block }, className)} src={url} title={title} width={width} onClick={onClick} />;
};
@@ -41,16 +41,16 @@ const styles = css`
interface Props {
message?: string | string[] | null;
status?: ENotificationType;
type?: ENotificationType | null;
exception?: Error | null;
className?: string;
onShowDetails?: () => void;
}
export const StatusMessage = observer<Props>(function StatusMessage({ status, message, exception = null, className, onShowDetails }) {
export const StatusMessage = observer<Props>(function StatusMessage({ type, message, exception = null, className, onShowDetails }) {
const translate = useTranslate();
const errorDetails = useErrorDetails(exception);
const isError = status === ENotificationType.Error || exception !== null;
const isError = type === ENotificationType.Error || exception !== null;
if (Array.isArray(message)) {
message = message.map(m => translate(m)).join(', ');
@@ -63,7 +63,7 @@ export const StatusMessage = observer<Props>(function StatusMessage({ status, me
if (isError) {
icon = '/icons/error_icon.svg';
} else if (status === ENotificationType.Success) {
} else if (type === ENotificationType.Success) {
icon = '/icons/success_icon.svg';
}
@@ -7,7 +7,7 @@
*/
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { s } from '../s';
import { useObjectRef } from '../useObjectRef';
@@ -95,8 +95,6 @@ export const Table = observer<React.PropsWithChildren<Props>>(function Table({
{ keys, isItemSelectable },
);
const isExpanded = useMemo(() => computed(() => Array.from(expanded.values()).some(Boolean)), [expanded]);
const setItemSelect = useCallback((item: any, state: boolean) => {
selected.set(item, state);
if (props.onSelect) {
@@ -119,7 +117,7 @@ export const Table = observer<React.PropsWithChildren<Props>>(function Table({
return (
<TableContext.Provider value={context}>
<table className={s(styles, { table: true, expanded: isExpanded.get(), big: size === 'big' }, className)}>{children}</table>
<table className={s(styles, { table: true, big: size === 'big' }, className)}>{children}</table>
</TableContext.Provider>
);
});
@@ -21,13 +21,14 @@ import { TableContext } from './TableContext';
import rowStyles from './TableItem.m.css';
import { ITableItemContext, TableItemContext } from './TableItemContext';
interface ExpandProps {
item: any;
export interface TableItemExpandProps<T> {
item: T;
onClose: () => void;
}
interface Props {
item: any;
expandElement?: React.FunctionComponent<ExpandProps>;
interface Props<T> extends React.PropsWithChildren {
item: T;
expandElement?: React.FunctionComponent<TableItemExpandProps<T>>;
selectOnItem?: boolean;
selectDisabled?: boolean;
disabled?: boolean;
@@ -37,7 +38,11 @@ interface Props {
onDoubleClick?: () => void;
}
export const TableItem = observer<React.PropsWithChildren<Props>>(function TableItem({
interface ITableItemComponent {
<T>(props: Props<T>): React.ReactElement<any, any> | null;
}
export const TableItem: ITableItemComponent = observer(function TableItem({
item,
expandElement,
selectOnItem,
@@ -101,6 +106,10 @@ export const TableItem = observer<React.PropsWithChildren<Props>>(function Table
[onDoubleClick],
);
const handleClose = useCallback(() => {
context.setItemExpand(item, false);
}, [context, item]);
const ExpandElement = expandElement;
return (
@@ -117,7 +126,7 @@ export const TableItem = observer<React.PropsWithChildren<Props>>(function Table
<tr className={s(styles, { noHover: true, expanded: isExpanded, row: true })}>
<td colSpan={Children.toArray(children).length} className={s(styles, { expandArea: true, cell: true })}>
<Loader suspense>
<ExpandElement item={item} />
<ExpandElement item={item} onClose={handleClose} />
</Loader>
</td>
</tr>
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useContext } from 'react';
import { useContext } from 'react';
import styled, { css } from 'reshadow';
import { EventContext } from '@cloudbeaver/core-events';
@@ -33,31 +33,22 @@ const styles = css`
export const TableItemSelect = observer<Props>(function TableItemSelect({ checked, disabled, tooltip, className }) {
const tableContext = useContext(TableContext);
const context = useContext(TableItemContext);
const selected = checked ?? context?.isSelected();
const handleClick = useCallback(
(event: React.MouseEvent<HTMLInputElement>) => {
if (!context) {
return;
}
const state = !context.isSelected();
EventContext.set(event, EventTableItemSelectionFlag, state);
function handleClick(event: React.MouseEvent<HTMLInputElement>) {
if (!context) {
return;
}
EventContext.set(event, EventTableItemSelectionFlag, !selected);
tableContext?.setItemSelect(context.item, state);
},
[tableContext, context],
);
tableContext?.setItemSelect(context.item, !selected);
}
if (!context) {
return null;
}
return styled(styles)(
<Checkbox
className={className}
title={tooltip}
disabled={context.selectDisabled || disabled}
checked={checked || context.isSelected()}
onClick={handleClick}
/>,
<Checkbox className={className} title={tooltip} disabled={context.selectDisabled || disabled} checked={selected} onClick={handleClick} />,
);
});
@@ -51,15 +51,10 @@ export class TableState {
selectedList: computed,
expandedList: computed,
unselect: action,
unexpand: action,
collapse: action,
});
}
expand(key: string, value: boolean) {
this.expanded.set(key, value);
this.onExpand.execute({ key, value });
}
unselect(key?: Key): Map<string, boolean> {
if (key === undefined) {
this.selected.clear();
@@ -76,7 +71,12 @@ export class TableState {
return this.selected;
}
unexpand(key?: Key): Map<string, boolean> {
expand(key: string, value: boolean) {
this.expanded.set(key, value);
this.onExpand.execute({ key, value });
}
collapse(key?: Key): Map<string, boolean> {
if (key === undefined) {
this.expanded.clear();
} else {
@@ -93,7 +93,7 @@ export class TableState {
}
reset() {
this.unexpand();
this.collapse();
this.unselect();
}
}
+5 -1
View File
@@ -155,7 +155,11 @@ export * from './FormControls/Radio';
export * from './FormControls/RadioGroup';
export * from './FormControls/RadioGroupContext';
export * from './FormControls/ShadowInput';
export * from './FormControls/SubmittingForm';
export * from './FormControls/Form';
export * from './FormControls/Textarea';
export * from './FormControls/useCapsLockTracker';
export * from './FormControls/useCustomInputValidation';
export * from './FormControls/useForm';
export * from './FormControls/Textarea';
export * from './Link';
export * from './Cell';
@@ -7,8 +7,8 @@
*/
import { useCallback } from 'react';
export function useCombinedRef<T>(...refs: React.ForwardedRef<T>[]) {
return useCallback((instance: T | null) => {
export function useCombinedRef<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
return useCallback(instance => {
for (const ref of refs) {
if (typeof ref === 'function') {
ref(instance);
+23 -1
View File
@@ -12,9 +12,11 @@ import type { IExecutor, IExecutorHandler, IExecutorHandlersCollection } from '@
import { useObjectRef } from './useObjectRef';
interface IUseExecutorOptions<T> {
executor?: IExecutor<T> | IExecutorHandlersCollection<T>;
executor?: IExecutorHandlersCollection<T>;
handlers?: Array<IExecutorHandler<T>>;
postHandlers?: Array<IExecutorHandler<T>>;
next?: IExecutorHandlersCollection<T>;
before?: IExecutorHandlersCollection<T>;
}
export function useExecutor<T>(options: IUseExecutorOptions<T>): void {
@@ -28,6 +30,8 @@ export function useExecutor<T>(options: IUseExecutorOptions<T>): void {
const handlers: Array<IExecutorHandler<T>> = [];
const postHandlers: Array<IExecutorHandler<T>> = [];
const next: IExecutorHandlersCollection<T> | undefined = props.next;
const before: IExecutorHandlersCollection<T> | undefined = props.before;
if (props.handlers) {
for (let i = 0; i < props.handlers.length; i++) {
@@ -36,6 +40,7 @@ export function useExecutor<T>(options: IUseExecutorOptions<T>): void {
handlers.push(handler);
}
}
if (props.postHandlers) {
for (let i = 0; i < props.postHandlers.length; i++) {
const handler: IExecutorHandler<T> = (data, contexts) => props.postHandlers?.[i](data, contexts);
@@ -44,13 +49,30 @@ export function useExecutor<T>(options: IUseExecutorOptions<T>): void {
}
}
if (props.next) {
executor.next(props.next);
}
if (props.before) {
executor.next(props.before);
}
return () => {
for (const handler of handlers) {
executor.removeHandler(handler);
}
for (const handler of postHandlers) {
executor.removePostHandler(handler);
}
if (next) {
executor.removeNext(next);
}
if (before) {
executor.removeBefore(before);
}
};
}, [executor, props.handlers?.length, props.postHandlers?.length]);
}
+7 -6
View File
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { action, observable } from 'mobx';
import { useEffect } from 'react';
import { useLayoutEffect } from 'react';
import { useObjectRef } from './useObjectRef';
import { useObservableRef } from './useObservableRef';
@@ -106,9 +106,8 @@ export function useFocus<T extends HTMLElement>({ autofocus, focusFirstChild, on
'useFocus',
);
useEffect(() => {
const reference = state.reference;
const reference = state.reference;
useLayoutEffect(() => {
if (!reference) {
return;
}
@@ -138,9 +137,11 @@ export function useFocus<T extends HTMLElement>({ autofocus, focusFirstChild, on
reference.removeEventListener('focusin', focusHandler);
reference.removeEventListener('focusout', blurHandler);
};
}, [state.reference]);
// TODO: probably we need to create a PR to https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks for custom stable hooks
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reference]);
useEffect(
useLayoutEffect(
() => () => {
state.restoreFocus();
},
+1 -1
View File
@@ -49,7 +49,7 @@ export function useS<T extends ComponentStyle[]>(...componentStyles: [...T]): Ex
const themeService = useService(ThemeService);
const [currentThemeId, setCurrentThemeId] = useState(() => themeService.currentThemeId);
const lastThemeRef = useRef<string>(currentThemeId);
//@ts-ignore
// @ts-ignore
const filteredStyles = themeService.mapStyles(componentStyles.flat(Infinity).filter(Boolean) as Style[], context);
const trackTheme = filteredStyles.some(style => typeof style === 'function');
@@ -20,6 +20,7 @@ const stylesCache = new MetadataMap<string, Map<ThemeSelector<any>, Promise<unde
* Changes styles depending on theme
*
* @param componentStyles styles array
* @deprecated use useS and css-modules instead
*/
export function useStyles(...componentStyles: ComponentStyle[]): Record<string, any> {
// todo do you understand that we store ALL STYLES in each component that uses this hook?
+20
View File
@@ -46,16 +46,36 @@ declare module 'reshadow' {
declare function taggedStyled(strs: TemplateStringsArray, ...values: any[]): IdFunction;
declare function taggedStyled<A extends JSX.Element>(node: A): A;
/**
*
* @deprecated use css-modules instead
*/
declare function styled(...styles: StylesMap[]): typeof taggedStyled;
/**
*
* @deprecated use css-modules instead
*/
declare function styled(strs: TemplateStringsArray, ...values: any[]): IdFunction;
/**
*
* @deprecated use css-modules instead
*/
export declare var use: ((value: StylesMap) => {}) & {
[key: string]: typeof Component;
};
/**
*
* @deprecated use css-modules instead
*/
export declare function css(strs: TemplateStringsArray, ...values: any[]): StylesMap;
export declare function keyframes(strs: TemplateStringsArray, ...values: any[]): string;
/**
*
* @deprecated use css-modules instead
*/
export declare function create(styles: StylesMap[]): StylesMap;
declare var jsx: typeof createElement;
+2 -2
View File
@@ -9,6 +9,6 @@ import { injectable } from './injectable';
@injectable()
export abstract class Bootstrap {
abstract register(): void | Promise<void>;
abstract load(): void | Promise<void>;
register(): void | Promise<void> {}
load(): void | Promise<void> {}
}
@@ -9,7 +9,7 @@ import { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useEffect } from 'react';
import { Button, Container, Fill, InputField, s, SubmittingForm, useFocus, useObservableRef, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { Button, Container, Fill, Form, InputField, s, useFocus, useObservableRef, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { throttleAsync } from '@cloudbeaver/core-utils';
import { CommonDialogBody } from './CommonDialog/CommonDialogBody';
@@ -97,13 +97,13 @@ export const RenameDialog: DialogComponent<RenameDialogPayload, string> = observ
<CommonDialogWrapper size="small" className={className} fixedWidth>
<CommonDialogHeader title={title} subTitle={subTitle} icon={icon} viewBox={viewBox} bigIcon={bigIcon} onReject={rejectDialog} />
<CommonDialogBody>
<SubmittingForm ref={focusedRef} onSubmit={() => resolveDialog(state.value)}>
<Form ref={focusedRef} onSubmit={() => resolveDialog(state.value)}>
<Container center>
<InputField name="value" state={state} error={!state.valid} description={errorMessage} onChange={() => state.validate()}>
{translate('ui_name') + ':'}
</InputField>
</Container>
</SubmittingForm>
</Form>
</CommonDialogBody>
<CommonDialogFooter className={s(styles, { footer: true })}>
<Button type="button" mod={['outlined']} onClick={rejectDialog}>
@@ -82,6 +82,10 @@ export class ExecutorHandlersCollection<T = unknown, TResult = any | Promise<any
return this;
}
removeBefore(executor: IExecutorHandlersCollection<any, TResult>): void {
this.chain = this.chain.filter(link => link.executor !== executor || link.type !== 'before');
}
next<TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>, filter?: ExecutorDataFilter<T>): this {
this.chain.push({
executor,
@@ -92,6 +96,10 @@ export class ExecutorHandlersCollection<T = unknown, TResult = any | Promise<any
return this;
}
removeNext(executor: IExecutorHandlersCollection<any, TResult>): void {
this.chain = this.chain.filter(link => link.executor !== executor || link.type !== 'next');
}
hasHandler(handler: IExecutorHandler<T, TResult>): boolean {
return this.handlers.includes(handler);
}
@@ -33,7 +33,9 @@ export interface IExecutorHandlersCollection<T = unknown, TResult = any | Promis
addContextCreator<TContext>(context: IAsyncContextLoader<TContext, T>, creator: IAsyncContextLoader<TContext, T>): this;
before: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>) => this;
removeBefore: (executor: IExecutorHandlersCollection<any, TResult>) => void;
next: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>) => this;
removeNext: (executor: IExecutorHandlersCollection<any, TResult>) => void;
addCollection: (collection: IExecutorHandlersCollection<T, TResult>) => this;
hasHandler: (handler: IExecutorHandler<T, TResult>) => boolean;
addHandler: (handler: IExecutorHandler<T, TResult>) => this;
@@ -17,9 +17,9 @@ export type CachedDataResourceGetter<TValue, TIncludes> = TValue extends null
? CachedResourceValueIncludes<TValue, TIncludes> | null
: CachedResourceValueIncludes<TValue, TIncludes>;
/**
* CachedDataResource is a resource that stores data that has no identifiers.
*/
/**
* CachedDataResource is a resource that stores data that has no identifiers.
*/
export abstract class CachedDataResource<
TData,
TKey = void,
@@ -78,11 +78,11 @@ export function getCachedDataResourceLoaderState<TData, TKey = void, TContext ex
isOutdated() {
return resource.isOutdated(param);
},
load() {
return resource.load(param, context);
async load() {
await resource.load(param, context);
},
reload() {
return resource.refresh(param, context);
async reload() {
await resource.refresh(param, context);
},
};
}
@@ -267,32 +267,32 @@ export abstract class CachedMapResource<
export function getCachedMapResourceLoaderState<TKey, TValue, TContext extends Record<string, any> = Record<string, never>>(
resource: CachedMapResource<TKey, TValue, TContext>,
key: ResourceKey<TKey>,
includes?: CachedResourceIncludeArgs<TValue, TContext> | undefined,
getKey: () => ResourceKey<TKey>,
getIncludes?: () => CachedResourceIncludeArgs<TValue, TContext> | undefined,
lazy?: boolean,
): ILoadableState {
return {
lazy,
get exception() {
return resource.getException(key);
return resource.getException(getKey());
},
isLoading() {
return resource.isLoading(key);
return resource.isLoading(getKey());
},
isLoaded() {
return resource.isLoaded(key, includes);
return resource.isLoaded(getKey(), getIncludes?.());
},
isError() {
return isContainsException(this.exception);
},
isOutdated() {
return resource.isOutdated(key);
return resource.isOutdated(getKey());
},
load() {
return resource.load(key, includes);
async load() {
await resource.load(getKey(), getIncludes?.());
},
reload() {
return resource.refresh(key, includes);
async reload() {
await resource.refresh(getKey(), getIncludes?.());
},
};
}
@@ -474,11 +474,11 @@ export function getCachedTreeResourceLoaderState<TValue, TContext extends Record
isOutdated() {
return resource.isOutdated(key);
},
load() {
return resource.load(key, includes);
async load() {
await resource.load(key, includes);
},
reload() {
return resource.refresh(key, includes);
async reload() {
await resource.refresh(key, includes);
},
};
}
@@ -7,6 +7,7 @@ query serverConfig {
rootURI
containerId
defaultAuthRole
defaultUserTeam
productConfiguration
supportsCustomConnections
supportsConnectionBrowser
@@ -13,4 +13,7 @@ export type ThemeSelector<T extends Record<string, string> = Record<string, stri
) => Promise<undefined | BaseStyles<T> | BaseStyles<T>[]>;
export type Style<T extends Record<string, string> = Record<string, string>> = BaseStyles<T> | ThemeSelector<T>;
export type DynamicStyle<T extends Record<string, string> = Record<string, string>> = Style<T> | boolean | undefined;
/**
* @deprecated use SContext and StyleRegistry instead
*/
export type ComponentStyle<T extends Record<string, string> = Record<string, string>> = DynamicStyle<T> | ComponentStyle<T>[];
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { createDataContext } from '@cloudbeaver/core-view';
import type { IFormState } from './IFormState';
export const DATA_CONTEXT_FORM_STATE = createDataContext<IFormState<any>>('Form State');
@@ -0,0 +1,87 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { PlaceholderContainer } from '@cloudbeaver/core-blocks';
import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
import { ExecutorHandlersCollection, ExecutorInterrupter, IExecutorHandler, type IExecutorHandlersCollection } from '@cloudbeaver/core-executor';
import type { LocalizationService } from '@cloudbeaver/core-localization';
import { TabsContainer } from '../Tabs/TabsContainer/TabsContainer';
import { formStatusContext } from './formStatusContext';
import { formValidationContext } from './formValidationContext';
import type { IFormProps } from './IFormProps';
import type { IFormState } from './IFormState';
export class FormBaseService<TState, TProps extends IFormProps<TState> = IFormProps<TState>> {
readonly parts: TabsContainer<TProps>;
readonly actionsContainer: PlaceholderContainer<TProps>;
readonly onConfigure: IExecutorHandlersCollection<IFormState<TState>>;
readonly onFillDefaultConfig: IExecutorHandlersCollection<IFormState<TState>>;
readonly onPrepareConfig: IExecutorHandlersCollection<TState>;
readonly onValidate: IExecutorHandlersCollection<IFormState<TState>>;
readonly onSubmit: IExecutorHandlersCollection<IFormState<TState>>;
readonly onState: IExecutorHandlersCollection<TState>;
constructor(private readonly localizationService: LocalizationService, private readonly notificationService: NotificationService, name: string) {
this.parts = new TabsContainer(name);
this.actionsContainer = new PlaceholderContainer();
this.onConfigure = new ExecutorHandlersCollection();
this.onFillDefaultConfig = new ExecutorHandlersCollection();
this.onPrepareConfig = new ExecutorHandlersCollection();
this.onValidate = new ExecutorHandlersCollection();
this.onSubmit = new ExecutorHandlersCollection();
this.onState = new ExecutorHandlersCollection();
this.onSubmit.before(this.onPrepareConfig);
this.onState.before(this.onPrepareConfig);
this.onSubmit.addPostHandler(this.handleSubmittingStatus);
this.onValidate.addPostHandler(this.handleValidation);
}
private readonly handleSubmittingStatus: IExecutorHandler<IFormState<TState>> = (data, contexts) => {
const status = contexts.getContext(formStatusContext);
if (!status.saved) {
ExecutorInterrupter.interrupt(contexts);
}
if (status.messages.length > 0) {
// if (status.exception) {
// this.notificationService.logException(status.exception, status.messages[0], status.messages.slice(1).join('\n'));
// } else {
// this.notificationService.notify(
// {
// title: status.messages[0],
// message: status.messages.slice(1).join('\n'),
// },
// status.saved ? ENotificationType.Success : ENotificationType.Error,
// );
// }
}
};
private readonly handleValidation: IExecutorHandler<IFormState<TState>> = (data, contexts) => {
const validation = contexts.getContext(formValidationContext);
if (!validation.valid) {
ExecutorInterrupter.interrupt(contexts);
}
if (validation.messages.length > 0) {
const messages = validation.messages.map(message => this.localizationService.translate(message));
this.notificationService.notify(
{
title: 'core_ui_form_save_error',
message: messages.join('\n'),
},
validation.valid ? ENotificationType.Info : ENotificationType.Error,
);
}
};
}
@@ -5,11 +5,8 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { IFormStateInfo } from '@cloudbeaver/core-blocks';
export interface IUserProfileFormState {
info: IFormStateInfo;
validate: () => Promise<void>;
dispose: () => void;
export enum FormMode {
Edit = 'edit',
Create = 'create',
}
@@ -0,0 +1,140 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { action, makeObservable, observable, toJS } from 'mobx';
import { executorHandlerFilter, type IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { isObjectsEqual } from '@cloudbeaver/core-utils';
import type { IFormPart } from './IFormPart';
import type { IFormState } from './IFormState';
export abstract class FormPart<TPartState, TFormState = any> implements IFormPart<TPartState> {
state: TPartState;
initialState: TPartState;
exception: Error | null;
promise: Promise<any> | null;
protected loaded: boolean;
protected loading: boolean;
constructor(protected readonly formState: IFormState<TFormState>, initialState: TPartState) {
this.initialState = initialState;
this.state = toJS(this.initialState);
this.exception = null;
this.promise = null;
this.loaded = false;
this.loading = false;
this.formState.submitTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.save.bind(this)));
this.formState.configureTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.configure.bind(this)));
this.formState.validationTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.validate.bind(this)));
makeObservable<this, 'loaded' | 'loading' | 'setInitialState'>(this, {
initialState: observable,
state: observable,
exception: observable.ref,
promise: observable.ref,
loaded: observable,
loading: observable,
setInitialState: action,
});
}
isLoading(): boolean {
return this.loading;
}
isOutdated(): boolean {
return false;
}
isLoaded(): boolean {
return this.loaded;
}
isError(): boolean {
return this.exception !== null;
}
isChanged(): boolean {
if (!this.loaded || this.initialState === this.state) {
return false;
}
return !isObjectsEqual(this.initialState, this.state);
}
async save(): Promise<any> {
if (this.loading) {
return;
}
this.loading = true;
try {
await this.loader();
if (!this.isChanged()) {
return;
}
await this.saveChanges();
this.loaded = false;
this.exception = null;
} catch (exception: any) {
this.exception = exception;
throw exception;
} finally {
this.loading = false;
}
}
async load(): Promise<void> {
if (this.loading) {
return this.promise;
}
try {
this.loading = true;
this.promise = this.loader();
await this.promise;
this.loaded = true;
this.exception = null;
} catch (exception: any) {
this.exception = exception;
} finally {
this.promise = null;
this.loading = false;
}
}
async reload(): Promise<void> {
this.loaded = false;
await this.load();
}
protected setInitialState(initialState: TPartState) {
this.initialState = initialState;
if (this.isChanged()) {
return;
}
this.state = toJS(this.initialState);
}
protected configure(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
protected validate(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
protected abstract loader(): Promise<void>;
protected abstract saveChanges(): Promise<void>;
}
@@ -0,0 +1,223 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { action, makeObservable, observable } from 'mobx';
import type { App } from '@cloudbeaver/core-di';
import type { ENotificationType } from '@cloudbeaver/core-events';
import { Executor, IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor';
import { isLoadableStateHasException, MetadataMap, uuid } from '@cloudbeaver/core-utils';
import {
DATA_CONTEXT_LOADABLE_STATE,
dataContextAddDIProvider,
type IDataContext,
loadableStateContext,
TempDataContext,
} from '@cloudbeaver/core-view';
import { DATA_CONTEXT_FORM_STATE } from './DATA_CONTEXT_FORM_STATE';
import type { FormBaseService } from './FormBaseService';
import { FormMode } from './FormMode';
import { formStateContext } from './formStateContext';
import type { IFormState } from './IFormState';
export class FormState<TState> implements IFormState<TState> {
mode: FormMode;
parts: MetadataMap<string, any>;
state: TState;
isDisabled: boolean;
statusMessage: string | string[] | null;
statusType: ENotificationType | null;
exception: Error | (Error | null)[] | null;
promise: Promise<any> | null;
readonly id: string;
readonly service: FormBaseService<TState, any>;
readonly dataContext: IDataContext;
readonly configureTask: IExecutor<IFormState<TState>>;
readonly formStateTask: IExecutor<TState>;
readonly fillDefaultConfigTask: IExecutor<IFormState<TState>>;
readonly submitTask: IExecutor<IFormState<TState>>;
readonly validationTask: IExecutor<IFormState<TState>>;
constructor(app: App, service: FormBaseService<TState, any>, state: TState) {
this.id = uuid();
this.service = service;
this.dataContext = new TempDataContext();
this.mode = FormMode.Create;
this.parts = new MetadataMap<string, any>();
this.state = state;
this.isDisabled = false;
this.statusMessage = null;
this.statusType = null;
this.exception = null;
this.promise = null;
this.configureTask = new Executor(this as IFormState<TState>, () => true);
this.configureTask.addCollection(service.onConfigure);
this.formStateTask = new Executor<TState>(state, () => true);
this.formStateTask.addCollection(service.onState).addPostHandler(this.updateFormState.bind(this));
this.fillDefaultConfigTask = new Executor(this as IFormState<TState>, () => true);
this.fillDefaultConfigTask.addCollection(service.onFillDefaultConfig).next(this.formStateTask, form => form.state);
this.validationTask = new Executor(this as IFormState<TState>, () => true);
this.validationTask.addCollection(service.onValidate);
this.submitTask = new Executor(this as IFormState<TState>, () => true);
this.submitTask.addCollection(service.onSubmit).before(this.validationTask);
this.dataContext.set(DATA_CONTEXT_LOADABLE_STATE, loadableStateContext());
this.dataContext.set(DATA_CONTEXT_FORM_STATE, this);
dataContextAddDIProvider(this.dataContext, app);
makeObservable<this>(this, {
mode: observable,
parts: observable.ref,
promise: observable.ref,
exception: observable.ref,
isDisabled: observable.ref,
state: observable,
setMode: action,
setPartsState: action,
setException: action,
setState: action,
});
}
isLoading(): boolean {
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isLoading());
}
isLoaded(): boolean {
if (this.promise) {
return false;
}
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.every(loader => loader.isLoaded());
}
isError(): boolean {
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isError());
}
isOutdated(): boolean {
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isOutdated?.() === true);
}
isCancelled(): boolean {
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isCancelled?.() === true);
}
async load(refresh?: boolean): Promise<void> {
if (this.promise !== null) {
return this.promise;
}
if (this.isLoaded() && !this.isOutdated() && !refresh) {
return;
}
this.promise = (async () => {
try {
this.isDisabled = true;
await this.configureTask.execute(this);
const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders;
for (const loader of loaders) {
if (isLoadableStateHasException(loader)) {
continue;
}
if (!loader.isLoaded() || loader.isOutdated?.() === true) {
try {
await loader.load();
} catch {
return;
}
}
}
await this.fillDefaultConfigTask.execute(this);
this.exception = null;
} catch (exception: any) {
this.exception = exception;
throw exception;
} finally {
this.isDisabled = false;
this.promise = null;
}
})();
}
async reload(): Promise<void> {
await this.load(true);
}
cancel(): void {
const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders;
for (const loader of loaders) {
if (loader.isCancelled?.() !== true) {
loader.cancel?.();
}
}
}
setMode(mode: FormMode): this {
this.mode = mode;
return this;
}
setPartsState(state: MetadataMap<string, any>): this {
this.parts = state;
return this;
}
setException(exception: Error | (Error | null)[] | null): this {
this.exception = exception;
return this;
}
setState(state: TState): this {
this.state = state;
return this;
}
async save(): Promise<boolean> {
try {
this.isDisabled = true;
await this.submitTask.execute(this);
this.exception = null;
return true;
} catch (exception: any) {
this.exception = exception;
} finally {
this.isDisabled = false;
}
return false;
}
private updateFormState(data: TState, contexts: IExecutionContextProvider<TState>): void {
const context = contexts.getContext(formStateContext);
if (this.mode === FormMode.Create) {
context.markEdited();
}
this.statusMessage = context.statusMessage;
this.statusType = context.statusType;
}
}
@@ -0,0 +1,18 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { ILoadableState } from '@cloudbeaver/core-utils';
export interface IFormPart<TState> extends ILoadableState {
state: TState;
initialState: TState;
isChanged(): boolean;
load(): Promise<void>;
save(): Promise<void>;
}
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { IFormState } from './IFormState';
export interface IFormProps<TState> {
readonly formState: IFormState<TState>;
}
@@ -0,0 +1,53 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { ENotificationType } from '@cloudbeaver/core-events';
import type { IExecutor } from '@cloudbeaver/core-executor';
import type { ILoadableState, MetadataMap } from '@cloudbeaver/core-utils';
import type { IDataContext } from '@cloudbeaver/core-view';
import type { FormBaseService } from './FormBaseService';
import type { FormMode } from './FormMode';
export interface IFormState<TState> extends ILoadableState {
readonly id: string;
readonly service: FormBaseService<TState>;
readonly dataContext: IDataContext;
readonly mode: FormMode;
readonly parts: MetadataMap<string, any>;
readonly state: TState;
readonly isDisabled: boolean;
readonly promise: Promise<any> | null;
readonly exception: Error | (Error | null)[] | null;
readonly statusMessage: string | string[] | null;
readonly statusType: ENotificationType | null;
readonly configureTask: IExecutor<IFormState<TState>>;
readonly formStateTask: IExecutor<TState>;
readonly fillDefaultConfigTask: IExecutor<IFormState<TState>>;
readonly submitTask: IExecutor<IFormState<TState>>;
readonly validationTask: IExecutor<IFormState<TState>>;
setMode(mode: FormMode): this;
setPartsState(state: MetadataMap<string, any>): this;
setException(exception: Error | (Error | null)[] | null): this;
setState(state: TState): this;
isLoading(): boolean;
isLoaded(): boolean;
isError(): boolean;
isOutdated(): boolean;
isCancelled(): boolean;
load(): Promise<void>;
reload(): Promise<void>;
save(): Promise<boolean>;
cancel(): void;
}
@@ -0,0 +1,16 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { ENotificationType } from '@cloudbeaver/core-events';
export interface IFormStateInfo {
edited: boolean;
disabled: boolean;
readonly: boolean;
statusMessage: string | string[] | null;
statusType: ENotificationType | null;
}
@@ -0,0 +1,55 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { ENotificationType } from '@cloudbeaver/core-events';
import type { IFormStateInfo } from './IFormStateInfo';
export interface IFormStateContext extends IFormStateInfo {
markEdited: () => void;
setInfo(message: string): void;
setError(message: string): void;
setSuccess(message: string): void;
setLoading(message: string): void;
setMessage(message: string, type: ENotificationType): void;
}
export function formStateContext(): IFormStateContext {
return {
edited: false,
disabled: false,
readonly: false,
statusMessage: null,
statusType: null,
markEdited() {
this.edited = true;
},
setInfo(message) {
this.setMessage(message, ENotificationType.Info);
},
setError(message) {
this.setMessage(message, ENotificationType.Error);
},
setSuccess(message) {
this.setMessage(message, ENotificationType.Success);
},
setLoading(message) {
this.setMessage(message, ENotificationType.Loading);
},
setMessage(message, type) {
if (typeof this.statusMessage === 'string') {
this.statusMessage = [this.statusMessage];
}
if (this.statusMessage === null) {
this.statusMessage = message;
} else {
this.statusMessage.push(message);
}
this.statusType = type;
},
};
}
@@ -0,0 +1,31 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export interface IFormStatusContext {
saved: boolean;
messages: string[];
exception: Error | null;
info: (message: string) => void;
error: (message: string, exception?: Error) => void;
}
export function formStatusContext(): IFormStatusContext {
return {
saved: true,
messages: [],
exception: null,
info(message: string) {
this.messages.push(message);
},
error(message: string, exception: Error | null = null) {
this.messages.push(message);
this.saved = false;
this.exception = exception;
},
};
}
@@ -0,0 +1,28 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export interface IFormValidationContext {
valid: boolean;
messages: string[];
info: (message: string) => void;
error: (message: string) => void;
}
export function formValidationContext(): IFormValidationContext {
return {
valid: true,
messages: [],
info(message: string) {
this.messages.push(message);
},
error(message: string) {
this.messages.push(message);
this.valid = false;
},
};
}
@@ -0,0 +1,10 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { createDataContext } from '@cloudbeaver/core-view';
export const DATA_CONTEXT_TABS_FORM_VALIDATION = createDataContext<string | null>('Tabs Form Validation', () => null);
@@ -5,7 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useMemo } from 'react';
import { useContext, useMemo } from 'react';
import styled from 'reshadow';
import { Translate, useStyles } from '@cloudbeaver/core-blocks';
@@ -13,6 +13,7 @@ import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { TabContext } from '../TabContext';
import type { ITabData } from '../TabsContainer/ITabsContainer';
import { TabsContext } from '../TabsContext';
import { Tab } from './Tab';
import { TabIcon } from './TabIcon';
import type { TabProps } from './TabProps';
@@ -42,8 +43,10 @@ export function TabDefault<T = Record<string, any>>({
onClose,
...rest
}: Props<T> & T): React.ReactElement | null {
const state = useContext(TabsContext);
const styles = useStyles(style);
const tabContext = useMemo(() => ({ tabId }), [tabId]);
const selected = state?.state.selectedId === tabId;
if (component) {
const TabComponent = component;
@@ -54,6 +57,7 @@ export function TabDefault<T = Record<string, any>>({
className={className}
{...(rest as unknown as T)}
style={style}
selected={selected}
disabled={disabled}
onOpen={onOpen}
onClose={onClose}
@@ -63,7 +67,7 @@ export function TabDefault<T = Record<string, any>>({
}
return styled(styles)(
<Tab tabId={tabId} className={className} style={style} disabled={disabled} onOpen={onOpen} onClose={onClose}>
<Tab tabId={tabId} className={className} style={style} selected={selected} disabled={disabled} onOpen={onOpen} onClose={onClose}>
{icon && <TabIcon icon={icon} />}
{name && (
<TabTitle>
@@ -16,6 +16,7 @@ export type TabProps = PropsWithChildren<{
tabId: string;
title?: string;
menuContext?: IDataContext;
selected?: boolean;
disabled?: boolean;
className?: string;
style?: ComponentStyle;
+10 -2
View File
@@ -11,18 +11,26 @@ import { useContext } from 'react';
import { useExecutor, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { TabContext } from '../TabContext';
import type { ITabData } from '../TabsContainer/ITabsContainer';
import { TabsContext } from '../TabsContext';
export function useTab(
tabId: string,
tabId?: string,
onOpen?: (tab: ITabData<any>) => Promise<void> | void,
onClose?: (tab: ITabData<any>) => Promise<void> | void,
onClick?: (tabId: string) => void,
) {
const state = useContext(TabsContext);
const tabContext = useContext(TabContext);
const refObject = useObjectRef({ onClick });
tabId = tabId || tabContext?.tabId;
if (tabId === undefined) {
throw new Error('Tab id not provided');
}
if (!state) {
throw new Error('TabsContext not provided');
}
@@ -71,7 +79,7 @@ export function useTab(
},
handleClose(e: React.MouseEvent<HTMLDivElement>) {
EventContext.set(e, EventStopPropagationFlag); // TODO: probably should use special flag
this.state.close(tabId);
this.state.close(this.tabId);
},
}),
{
+5 -3
View File
@@ -9,6 +9,7 @@ import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import { TabList as BaseTabList, TabListOptions, TabStateReturn } from 'reakit/Tab';
import { useTranslate } from '@cloudbeaver/core-blocks';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { generateTabElement } from './generateTabElement';
@@ -22,8 +23,9 @@ interface Props extends Omit<TabListOptions, keyof TabStateReturn> {
className?: string;
}
export const TabList = observer<React.PropsWithChildren<Props>>(function TabList({ style, className, children, childrenFirst, ...props }) {
export const TabList = observer<React.PropsWithChildren<Props>>(function TabList({ style, children, childrenFirst, ...props }) {
const state = useContext(TabsContext);
const translate = useTranslate();
if (!state) {
throw new Error('Tabs context was not provided');
@@ -32,7 +34,7 @@ export const TabList = observer<React.PropsWithChildren<Props>>(function TabList
if (state.container) {
const displayed = state.container.getDisplayed(state.props);
return (
<BaseTabList {...props} {...state.state} className={className} area-label={props['aria-label'] ?? state.container.areaLabel}>
<BaseTabList {...props} {...state.state} area-label={translate(props['aria-label'] ?? state.container.areaLabel)}>
{childrenFirst && children}
{displayed
.map(
@@ -62,7 +64,7 @@ export const TabList = observer<React.PropsWithChildren<Props>>(function TabList
}
return (
<BaseTabList {...props} className={className} {...state.state}>
<BaseTabList {...props} {...state.state} area-label={translate(props['aria-label'])}>
{children}
</BaseTabList>
);
+36 -5
View File
@@ -6,24 +6,55 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useContext, useMemo } from 'react';
import { useContext, useEffect, useMemo, useRef } from 'react';
import { TabPanel as BaseTabPanel, TabStateReturn } from 'reakit/Tab';
import { getComputed, Loader } from '@cloudbeaver/core-blocks';
import { FormContext, getComputed, Loader } from '@cloudbeaver/core-blocks';
import { DATA_CONTEXT_TABS_FORM_VALIDATION } from './DATA_CONTEXT_TABS_FORM_VALIDATION';
import { TabContext } from './TabContext';
import type { TabPanelProps } from './TabPanelProps';
import { TabsContext } from './TabsContext';
export const TabPanel: React.FC<TabPanelProps> = observer(function TabPanel({ tabId, children, className, lazy }) {
const state = useContext(TabsContext);
const formContext = useContext(FormContext);
const panelRef = useRef<HTMLDivElement>(null);
if (!state) {
throw new Error('Tabs context was not provided');
}
const validationTabId = state.context.get(DATA_CONTEXT_TABS_FORM_VALIDATION);
const tabContext = useMemo(() => ({ tabId }), [tabId]);
const enabled = getComputed(() => (lazy || state.lazy) && state.state.selectedId !== tabId);
const selected = getComputed(() => state.state.selectedId === tabId);
const enabled = getComputed(() => (lazy || state.lazy) && !selected);
useEffect(() => {
if (formContext === null) {
return;
}
async function trackValidity(event: Event) {
if (selected || state?.context.get(DATA_CONTEXT_TABS_FORM_VALIDATION) !== null) {
return;
}
state?.context.set(DATA_CONTEXT_TABS_FORM_VALIDATION, tabId);
await state?.open(tabId);
}
panelRef.current?.addEventListener('invalid', trackValidity, true);
return () => {
panelRef.current?.removeEventListener('invalid', trackValidity, true);
};
});
useEffect(() => {
if (selected && validationTabId === tabId) {
formContext?.validate();
state?.context.set(DATA_CONTEXT_TABS_FORM_VALIDATION, null);
}
});
if (enabled) {
return null;
@@ -32,7 +63,7 @@ export const TabPanel: React.FC<TabPanelProps> = observer(function TabPanel({ ta
if (typeof children === 'function') {
return (
<TabContext.Provider value={tabContext}>
<BaseTabPanel {...state.state} tabId={tabId} className={className}>
<BaseTabPanel ref={panelRef} {...state.state} tabId={tabId} className={className}>
<Loader suspense>{(children as (state: TabStateReturn) => React.ReactNode)(state.state)}</Loader>
</BaseTabPanel>
</TabContext.Provider>
@@ -41,7 +72,7 @@ export const TabPanel: React.FC<TabPanelProps> = observer(function TabPanel({ ta
return (
<TabContext.Provider value={tabContext}>
<BaseTabPanel {...state.state} tabId={tabId} className={className}>
<BaseTabPanel ref={panelRef} {...state.state} tabId={tabId} className={className}>
<Loader suspense>{children}</Loader>
</BaseTabPanel>
</TabContext.Provider>
@@ -5,7 +5,8 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
import type { ILoadableState, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
import type { IDataContextProvider } from '@cloudbeaver/core-view';
import type { TabProps } from '../Tab/TabProps';
@@ -15,7 +16,7 @@ export interface ITabData<T = void> {
}
export type TabContainerTabComponent<TProps = void> = React.FC<TabProps & TProps>;
export type TabContainerPanelComponent<TProps = void> = React.FC<{ tabId: string } & TProps>;
export type TabContainerPanelComponent<TProps = void> = React.FC<{ tabId: string; className?: string } & TProps>;
export interface ITabInfoOptions<TProps = void, TOptions extends Record<string, any> = never> {
key: string;
@@ -31,6 +32,7 @@ export interface ITabInfoOptions<TProps = void, TOptions extends Record<string,
panel: () => TabContainerPanelComponent<TProps> | React.ExoticComponent;
stateGetter?: (props: TProps) => MetadataValueGetter<string, any>;
getLoader?: (context: IDataContextProvider, props?: TProps) => ILoadableState[] | ILoadableState;
isHidden?: (tabId: string, props?: TProps) => boolean;
isDisabled?: (tabId: string, props?: TProps) => boolean;
@@ -49,6 +51,7 @@ export interface ITabsContainer<TProps = void, TOptions extends Record<string, a
readonly selectedId: string | null;
has: (tabId: string) => boolean;
getTabInfo: (tabId: string) => ITabInfo<TProps, TOptions> | undefined;
getDisplayedTabInfo: (tabId: string, props?: TProps) => ITabInfo<TProps, TOptions> | undefined;
getTabState: <T>(state: MetadataMap<string, any>, tabId: string, props: TProps, valueGetter?: MetadataValueGetter<string, T>) => T;
getDisplayed: (props?: TProps) => Array<ITabInfo<TProps, TOptions>>;
getIdList: (props?: TProps) => string[];
@@ -46,7 +46,7 @@ export class TabsContainer<TProps = void, TOptions extends Record<string, any> =
return;
}
const info = this.getTabInfo(tabId);
const info = this.getDisplayedTabInfo(tabId, props);
if (!info) {
return;
@@ -65,7 +65,7 @@ export class TabsContainer<TProps = void, TOptions extends Record<string, any> =
}
getTabState<T>(state: MetadataMap<string, any>, tabId: string, props: TProps, valueGetter?: MetadataValueGetter<string, T>): T {
const tabInfo = this.getTabInfo(tabId);
const tabInfo = this.getDisplayedTabInfo(tabId, props);
return state.get(tabId, valueGetter || tabInfo?.stateGetter?.(props));
}
@@ -74,6 +74,30 @@ export class TabsContainer<TProps = void, TOptions extends Record<string, any> =
return this.tabInfoList.filter(tabInfo => !tabInfo.isHidden?.(tabInfo.key, props));
}
getDisplayedTabInfo(tabId: string, props?: TProps): ITabInfo<TProps, TOptions> | undefined {
if (this.tabInfoMap.has(tabId)) {
return this.getTabInfo(tabId);
}
const displayed = this.getDisplayed(props);
for (const tabInfo of displayed) {
if (tabInfo.generator) {
const generated = tabInfo.generator(tabInfo.key, props);
if (generated.includes(tabId)) {
return tabInfo;
}
} else {
if (tabInfo.key === tabId) {
return tabInfo;
}
}
}
return undefined;
}
getIdList(props?: TProps): string[] {
return this.getDisplayed(props)
.map(tabInfo => {
@@ -10,6 +10,7 @@ import type { TabStateReturn } from 'reakit/Tab';
import type { IExecutor } from '@cloudbeaver/core-executor';
import type { MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
import type { IDataContext } from '@cloudbeaver/core-view';
import type { ITabData, ITabInfo, ITabsContainer } from './TabsContainer/ITabsContainer';
@@ -26,6 +27,7 @@ export interface ITabsContext<T = Record<string, any>> {
closable: boolean;
tabList?: string[];
enabledBaseActions?: boolean;
context: IDataContext;
canClose: (tabId: string) => boolean;
getTabInfo: (tabId: string) => ITabInfo<T> | undefined;
getTabState: <T>(tabId: string, valueGetter?: MetadataValueGetter<string, T>) => T;
+31 -4
View File
@@ -10,11 +10,12 @@ import { observer } from 'mobx-react-lite';
import { useEffect, useMemo, useState } from 'react';
import { useTabState } from 'reakit/Tab';
import { useExecutor, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
import { useAutoLoad, useExecutor, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor';
import { isNull, isUndefined, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
import { isDefined, isNull, isUndefined, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
import { useDataContext } from '@cloudbeaver/core-view';
import type { ITabData, ITabsContainer } from './TabsContainer/ITabsContainer';
import type { ITabData, ITabInfo, ITabsContainer } from './TabsContainer/ITabsContainer';
import { ITabsContext, TabsContext } from './TabsContext';
import type { TabDirection } from './TabsContext';
@@ -54,6 +55,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
canClose,
...rest
}: Props<T>): React.ReactElement | null {
const context = useDataContext();
const props = useMemo(() => rest as any as T, [...Object.values(rest)]);
let displayed: string[] = [];
@@ -168,6 +170,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
const value = useObservableRef<ITabsContext<T>>(
() => ({
context,
canClose(tabId) {
return (
dynamic.canClose?.({
@@ -177,7 +180,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
);
},
getTabInfo(tabId: string) {
return dynamic.container?.getTabInfo(tabId);
return dynamic.container?.getDisplayedTabInfo(tabId, dynamic.props);
},
getTabState(tabId: string, valueGetter?: MetadataValueGetter<string, any>) {
return dynamic.container?.getTabState(dynamic.tabsState, tabId, dynamic.props, valueGetter);
@@ -266,5 +269,29 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
},
);
if (container) {
let currentTabInfo: ITabInfo<T, never> | undefined;
if (state.selectedId) {
currentTabInfo = value.getTabInfo(state.selectedId);
}
useAutoLoad(
TabsState,
container
.getDisplayed(props)
.map(tab => tab.getLoader?.(context, props))
.filter(isDefined)
.flat(),
);
useAutoLoad(
TabsState,
[currentTabInfo?.getLoader?.(context, props) || []].flat().filter(loader => loader.lazy),
true,
true,
);
}
return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;
});
+13
View File
@@ -15,6 +15,19 @@ export * from './DragAndDrop/DNDProvider';
export * from './DragAndDrop/useDNDBox';
export * from './DragAndDrop/useDNDData';
export * from './Form/DATA_CONTEXT_FORM_STATE';
export * from './Form/FormBaseService';
export * from './Form/FormMode';
export * from './Form/FormState';
export * from './Form/FormPart';
export * from './Form/formStateContext';
export * from './Form/formStatusContext';
export * from './Form/formValidationContext';
export * from './Form/IFormPart';
export * from './Form/IFormProps';
export * from './Form/IFormState';
export * from './Form/IFormStateInfo';
export * from './InlineEditor/InlineEditor';
export * from './Screens/AppScreen/NavigationService';
+4 -1
View File
@@ -1 +1,4 @@
export default [['core_ui_empty', 'Empty']];
export default [
['core_ui_empty', 'Empty'],
['core_ui_form_save_error', 'Error occurred when saving the form'],
];
+4 -1
View File
@@ -1 +1,4 @@
export default [['core_ui_empty', 'Empty']];
export default [
['core_ui_empty', 'Empty'],
['core_ui_form_save_error', 'Error occurred when saving the form'],
];
+4 -1
View File
@@ -1 +1,4 @@
export default [['core_ui_empty', 'Пусто']];
export default [
['core_ui_empty', 'Пусто'],
['core_ui_form_save_error', 'Ошибка при сохранении формы'],
];
@@ -12,9 +12,10 @@ export interface ILoadableState {
isLoaded: () => boolean;
isError: () => boolean;
readonly exception?: (Error | null)[] | Error | null;
load: () => void;
reload?: () => void;
load: () => void | Promise<void>;
reload?: () => void | Promise<void>;
promise?: Promise<any> | null;
isOutdated?: () => boolean;
isCancelled?: () => boolean;
cancel?: () => void;
@@ -35,3 +36,11 @@ export function isContainsException(exception?: (Error | null)[] | Error | null)
return !!exception;
}
export function getFirstException(exception?: (Error | null)[] | Error | null): Error | null {
if (Array.isArray(exception)) {
return exception.find(Boolean) || null;
}
return exception || null;
}
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { createAction } from '../createAction';
export const ACTION_CREATE = createAction('create', {
label: 'core_view_action_create_label',
});
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { IServiceInjector } from '@cloudbeaver/core-di';
import { createDataContext } from './createDataContext';
export const DATA_CONTEXT_DI_PROVIDER = createDataContext<IServiceInjector>('DI Provider');
@@ -120,10 +120,11 @@ export class DataContext implements IDataContext {
}
get<T>(context: DataContextGetter<T>): T {
if (!this.map.has(context)) {
const defaultValue = context();
if (!this.hasOwn(context)) {
const defaultValue = context(this);
if (defaultValue !== undefined) {
this.set(context, defaultValue);
return defaultValue;
}
@@ -5,5 +5,6 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { IDataContextProvider } from './IDataContextProvider';
export type DataContextGetter<T> = () => T;
export type DataContextGetter<T> = (provider: IDataContextProvider) => T;
@@ -132,9 +132,10 @@ export class TempDataContext implements IDataContext {
get<T>(context: DataContextGetter<T>): T {
if (!this.hasOwn(context)) {
const defaultValue = context();
const defaultValue = context(this);
if (defaultValue !== undefined) {
this.set(context, defaultValue);
return defaultValue;
}
@@ -6,12 +6,16 @@
* you may not use this file except in compliance with the License.
*/
import type { DataContextGetter } from './DataContextGetter';
import type { IDataContextProvider } from './IDataContextProvider';
export function createDataContext<T>(name: string, defaultValue?: () => T extends any ? T : undefined): DataContextGetter<T> {
export function createDataContext<T>(
name: string,
defaultValue?: (context: IDataContextProvider) => T extends any ? T : undefined,
): DataContextGetter<T> {
name = `@context/${name}`;
const obj = {
[name](): T {
return defaultValue?.() as T;
[name](context: IDataContextProvider): T {
return defaultValue?.(context) as T;
},
};
return obj[name];
@@ -0,0 +1,16 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { App } from '@cloudbeaver/core-di';
import { DATA_CONTEXT_DI_PROVIDER } from './DATA_CONTEXT_DI_PROVIDER';
import type { IDataContext } from './IDataContext';
export function dataContextAddDIProvider(context: IDataContext, app: App): IDataContext {
context.set(DATA_CONTEXT_DI_PROVIDER, app.getServiceInjector());
return context;
}
@@ -5,18 +5,25 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { flat, ILoadableState } from '@cloudbeaver/core-utils';
import { createDataContext } from '../DataContext/createDataContext';
interface ILoadableStateContext {
readonly loaders: ILoadableState[];
getState(id: string, defaultState: () => ILoadableState | ILoadableState[]): ILoadableState[];
removeState(id: string): void;
}
export function loadableStateContext(): ILoadableStateContext {
const state = new Map<string, ILoadableState[]>();
const state = observable(new Map<string, ILoadableState[]>(), { deep: false });
return {
get loaders(): ILoadableState[] {
return flat([...state.values()]);
},
getState(id, defaultState) {
if (!state.has(id)) {
state.set(id, flat([defaultState()]));
@@ -24,6 +31,9 @@ export function loadableStateContext(): ILoadableStateContext {
return state.get(id)!;
},
removeState(id) {
state.delete(id);
},
};
}
@@ -20,7 +20,7 @@ export function useMenuContext(menu: IMenu, _menuContext?: IDataContext): IDataC
context.set(DATA_CONTEXT_MENU, menu);
if (context.has(DATA_CONTEXT_LOADABLE_STATE, false)) {
if (!context.has(DATA_CONTEXT_LOADABLE_STATE, false)) {
context.set(DATA_CONTEXT_LOADABLE_STATE, loadableStateContext());
}
+3
View File
@@ -1,4 +1,5 @@
export * from './Action/Actions/ACTION_COLLAPSE_ALL';
export * from './Action/Actions/ACTION_CREATE';
export * from './Action/Actions/ACTION_DELETE';
export * from './Action/Actions/ACTION_EDIT';
export * from './Action/Actions/ACTION_EXPORT';
@@ -31,7 +32,9 @@ export * from './Action/KeyBinding/IKeyBinding';
export * from './Action/KeyBinding/getCommonAndOSSpecificKeys';
export * from './Action/KeyBinding/getBindingLabel';
export * from './DataContext/createDataContext';
export * from './DataContext/DATA_CONTEXT_DI_PROVIDER';
export * from './DataContext/DataContext';
export * from './DataContext/dataContextAddDIProvider';
export * from './DataContext/DynamicDataContext';
export * from './DataContext/IDataContext';
export * from './DataContext/IDataContextProvider';
@@ -2,6 +2,7 @@ export default [
['core_view_layout', 'Layout'],
['core_view_zoom_in', 'Zoom In'],
['core_view_zoom_out', 'Zoom out'],
['core_view_action_create_label', 'Create'],
['core_view_action_new_folder', 'New folder'],
['core_view_action_edit_label', 'Edit'],
];
@@ -2,6 +2,7 @@ export default [
['core_view_layout', 'Layout'],
['core_view_zoom_in', 'Zoom In'],
['core_view_zoom_out', 'Zoom out'],
['core_view_action_create_label', 'Create'],
['core_view_action_new_folder', 'New folder'],
['core_view_action_edit_label', 'Edit'],
];
@@ -2,6 +2,7 @@ export default [
['core_view_layout', 'Упорядочить'],
['core_view_zoom_in', 'Приблизить'],
['core_view_zoom_out', 'Отдалить'],
['core_view_action_create_label', 'Создать'],
['core_view_action_new_folder', 'Создать папку'],
['core_view_action_edit_label', 'Изменить'],
];
@@ -59,7 +59,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-for-in-array": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { "destructuredArrayIgnorePattern": "^_" }],
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-function": "off",
@@ -12,12 +12,12 @@ import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentComponent,
import {
ColoredContainer,
Container,
Form,
Group,
GroupItem,
GroupTitle,
Loader,
Placeholder,
SubmittingForm,
ToolsAction,
ToolsPanel,
useFocus,
@@ -38,7 +38,7 @@ import { ServerConfigurationDriversForm } from './ServerConfigurationDriversForm
import { ServerConfigurationService } from './ServerConfigurationService';
const styles = css`
SubmittingForm {
Form {
flex: 1;
display: flex;
overflow: auto;
@@ -97,7 +97,7 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser
}
return styled(style)(
<SubmittingForm ref={focusedRef} name="server_config" onChange={handleChange}>
<Form ref={focusedRef} name="server_config" onChange={handleChange}>
{!configurationWizard && (
<ToolsPanel>
<ToolsAction
@@ -153,6 +153,6 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser
}
</Loader>
</ColoredContainer>
</SubmittingForm>,
</Form>,
);
});
@@ -81,7 +81,7 @@ export function useConfigurationsTable(): Readonly<State> {
await resource.deleteConfiguration(resourceKeyList(deletionList));
this.tableState.unselect();
this.tableState.unexpand(deletionList);
this.tableState.collapse(deletionList);
} catch (exception: any) {
notificationService.logException(exception, 'Configurations delete failed');
} finally {
@@ -14,12 +14,12 @@ import {
ColoredContainer,
Combobox,
FieldCheckbox,
Form,
Group,
GroupTitle,
InputField,
Link,
ObjectPropertyInfoForm,
SubmittingForm,
Textarea,
useClipboard,
useObjectPropertyCategories,
@@ -33,7 +33,7 @@ import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import type { IAuthConfigurationFormProps } from '../IAuthConfigurationFormProps';
const styles = css`
SubmittingForm {
Form {
flex: 1;
overflow: auto;
}
@@ -62,7 +62,7 @@ export const AuthConfigurationOptions: TabContainerPanelComponent<IAuthConfigura
}, [state]);
return styled(style)(
<SubmittingForm ref={formRef}>
<Form ref={formRef}>
<ColoredContainer parent gap overflow>
<Group small gap>
<Combobox
@@ -178,6 +178,6 @@ export const AuthConfigurationOptions: TabContainerPanelComponent<IAuthConfigura
</Group>
)}
</ColoredContainer>
</SubmittingForm>,
</Form>,
);
});
@@ -51,7 +51,7 @@ export const AuthenticationProviders: PlaceholderComponent<IConfigurationPlaceho
const isAnonymousAccessDisabled = authSettingsService.settings.getValue('disableAnonymousAccess');
useExecutor({
executor: formContext.changeExecutor,
executor: formContext.onChange,
handlers: [
function switchControls() {
if (serverConfig.enabledAuthProviders?.length === 0) {
@@ -0,0 +1,9 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export const ADMINISTRATION_ITEM_USER_CREATE_PARAM = 'create';
@@ -1,62 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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-blocks';
import type { AdminUserInfo } from '@cloudbeaver/core-sdk';
import { UserForm } from '../UserForm/UserForm';
const styles = css`
user-create-footer,
user-create-content {
composes: theme-background-secondary theme-text-on-secondary from global;
}
user-create {
display: flex;
flex-direction: column;
height: 540px;
overflow: hidden;
}
title-bar {
composes: theme-border-color-background theme-typography--headline6 from global;
box-sizing: border-box;
padding: 16px 24px;
align-items: center;
display: flex;
font-weight: 400;
flex: auto 0 0;
}
user-create-content {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
overflow: auto;
}
`;
interface Props {
user: AdminUserInfo;
onCancel: () => void;
}
export const CreateMetaParameter: React.FC<Props> = function CreateMetaParameter({ user, onCancel }) {
return styled(styles)(
<user-create>
<title-bar>
<Translate token="authentication_administration_user_connections_user_add" />
</title-bar>
<user-create-content>
<UserForm user={user} onCancel={onCancel} />
</user-create-content>
</user-create>,
);
};
@@ -1,56 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { makeObservable, observable } from 'mobx';
import { UsersResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import type { AdminUserInfo } from '@cloudbeaver/core-sdk';
import { UsersAdministrationNavigationService } from '../UsersAdministrationNavigationService';
@injectable()
export class CreateMetaParameterService {
user: AdminUserInfo | null = null;
constructor(
private readonly usersAdministrationNavigationService: UsersAdministrationNavigationService,
private readonly usersResource: UsersResource,
) {
makeObservable(this, {
user: observable,
});
this.user = null;
this.clearUserTemplate = this.clearUserTemplate.bind(this);
this.cancelCreate = this.cancelCreate.bind(this);
this.create = this.create.bind(this);
}
cancelCreate(): void {
this.clearUserTemplate();
this.usersAdministrationNavigationService.navToRoot();
}
create(): void {
if (this.user) {
return;
}
this.user = this.usersResource.getEmptyUser();
this.usersAdministrationNavigationService.navToCreate();
}
clearUserTemplate(): void {
this.user = null;
}
close(): void {
this.clearUserTemplate();
}
}
@@ -1,131 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { IconOrImage, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import type { UserInfo } from '@cloudbeaver/core-sdk';
import { BASE_TAB_STYLES, TabList, TabsState, UNDERLINE_TAB_BIG_STYLES, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui';
import type { IUserProfileFormState } from './IUserProfileFormState';
import { MetaParameterInfoPanel } from './UserInfo/MetaParameterInfoPanel';
import { MetaParameterInfoTab } from './UserInfo/MetaParameterInfoTab';
const tabsStyles = css`
TabList {
position: relative;
flex-shrink: 0;
align-items: center;
}
`;
const topBarStyles = css`
top-bar {
composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global;
position: relative;
display: flex;
padding-top: 16px;
&:before {
content: '';
position: absolute;
bottom: 0;
width: 100%;
border-bottom: solid 2px;
border-color: inherit;
}
}
top-bar-tabs {
flex: 1;
}
top-bar-actions {
display: flex;
align-items: center;
padding: 0 24px;
gap: 16px;
}
/*Button:not(:first-child) {
margin-right: 24px;
}*/
status-message {
composes: theme-typography--caption from global;
height: 24px;
padding: 0 16px;
display: flex;
align-items: center;
gap: 8px;
& IconOrImage {
height: 24px;
width: 24px;
}
}
`;
const formStyles = css`
box {
composes: theme-background-secondary theme-text-on-secondary from global;
}
flex-box {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
overflow: auto;
}
content-box {
composes: theme-background-secondary theme-border-color-background from global;
position: relative;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
`;
interface Props {
user: UserInfo;
state: IUserProfileFormState;
}
export const MetaParameterForm = observer<Props>(function MetaParameterForm({ user, state }) {
const translate = useTranslate();
const style = [BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES, UNDERLINE_TAB_BIG_STYLES];
const styles = useStyles(style, topBarStyles, formStyles);
return styled(styles)(
<flex-box>
<TabsState>
<top-bar>
<top-bar-tabs>
<status-message>
{state.info.statusMessage && (
<>
<IconOrImage icon="/icons/info_icon.svg" />
{translate(state.info.statusMessage)}
</>
)}
</status-message>
<TabList style={style} disabled={state.info.disabled}>
<MetaParameterInfoTab style={style} />
{/* <UserAuthProvidersTab style={style} /> */}
</TabList>
</top-bar-tabs>
<top-bar-actions> </top-bar-actions>
</top-bar>
<content-box>
<MetaParameterInfoPanel user={user} style={style} />
{/* <UserAuthProviderPanel user={user} style={style} /> */}
</content-box>
</TabsState>
</flex-box>,
);
});
@@ -1,35 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { UserMetaParametersResource } from '@cloudbeaver/core-authentication';
import { Container, ObjectPropertyInfoForm, useResource, useStyles } from '@cloudbeaver/core-blocks';
import type { UserInfo } from '@cloudbeaver/core-sdk';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
interface Props {
user: UserInfo;
className?: string;
style?: ComponentStyle;
}
export const MetaParameterInfoForm = observer<Props>(function MetaParameterInfoForm({ user, className, style }) {
const userMetaParameters = useResource(MetaParameterInfoForm, UserMetaParametersResource, undefined);
const styles = useStyles(style);
if (!userMetaParameters.data.length) {
return null;
}
return styled(styles)(
<Container className={className} wrap gap>
<ObjectPropertyInfoForm state={user.metaParameters} properties={userMetaParameters.data} category={null} disabled readOnly />
</Container>,
);
});
@@ -1,32 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { useStyles } from '@cloudbeaver/core-blocks';
import type { UserInfo } from '@cloudbeaver/core-sdk';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { TabPanel } from '@cloudbeaver/core-ui';
import { MetaParametersForm } from './MetaParametersForm';
interface Props {
user: UserInfo;
className?: string;
style?: ComponentStyle;
}
export const MetaParameterInfoPanel = observer<Props>(function MetaParameterInfoPanel({ user, className, style }) {
const styles = useStyles(style);
return styled(styles)(
<TabPanel tabId="info" className={className}>
<MetaParametersForm user={user} />
</TabPanel>,
);
});
@@ -1,28 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { Tab, TabTitle } from '@cloudbeaver/core-ui';
interface Props {
className?: string;
style?: ComponentStyle;
}
export const MetaParameterInfoTab = observer<Props>(function MetaParameterInfoTab({ className, style }) {
const translate = useTranslate();
const styles = useStyles(style);
return styled(styles)(
<Tab tabId="info" style={style} className={className}>
<TabTitle>{translate('plugin_user_profile_info')}</TabTitle>
</Tab>,
);
});
@@ -1,48 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { ColoredContainer, Container, Group, InputField, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import type { UserInfo } from '@cloudbeaver/core-sdk';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { AuthProvidersList } from '@cloudbeaver/plugin-user-profile';
import { MetaParameterInfoForm } from './MetaParameterInfoForm';
interface Props {
user: UserInfo;
className?: string;
style?: ComponentStyle;
}
export const MetaParametersForm = observer<Props>(function MetaParametersForm({ user, className, style }) {
const styles = useStyles(style);
const translate = useTranslate();
return styled(styles)(
<ColoredContainer wrap overflow parent gap>
<Container medium gap>
<Group form gap>
<Container wrap gap>
<InputField type="text" name="userId" minLength={1} state={user} mod="surface" disabled readOnly required tiny fill>
{translate('plugin_user_profile_info_id')}
</InputField>
<InputField type="text" name="displayName" minLength={1} state={user} mod="surface" disabled readOnly required tiny fill>
{translate('plugin_user_profile_info_displayName')}
</InputField>
</Container>
<MetaParameterInfoForm user={user} />
</Group>
<Group box medium overflow>
<AuthProvidersList user={user} providers={user.linkedAuthProviders} />
</Group>
</Container>
</ColoredContainer>,
);
});
@@ -1,25 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { IFormStateInfo } from '@cloudbeaver/core-blocks';
import type { IUserProfileFormState } from './IUserProfileFormState';
export class UserProfileFormState implements IUserProfileFormState {
get info(): IFormStateInfo {
return {
disabled: false,
edited: false,
readonly: false,
statusMessage: null,
};
}
async validate(): Promise<void> {}
dispose(): void {}
}
@@ -1,43 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useContext } from 'react';
import styled, { css } from 'reshadow';
import { Loader, TableContext, useStyles } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { UserForm } from '../UserForm/UserForm';
import { MetaParameterEditController } from './MetaParameterEditController';
const styles = css`
box {
composes: theme-background-secondary theme-text-on-secondary from global;
box-sizing: border-box;
padding-bottom: 24px;
height: 520px;
display: flex;
flex-direction: column;
}
`;
interface Props {
item: string;
}
export const MetaParameterEdit = observer<Props>(function UserEdit({ item }) {
const controller = useController(MetaParameterEditController, item);
const tableContext = useContext(TableContext);
const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext]);
return styled(useStyles(styles))(
<box>
{controller.user ? <UserForm user={controller.user} editing onCancel={collapse} /> : <Loader />}
</box>,
);
});
@@ -1,65 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { computed, makeObservable, observable } from 'mobx';
import { UsersResource } from '@cloudbeaver/core-authentication';
import { IDestructibleController, IInitializableController, injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { AdminUserInfo, GQLErrorCatcher, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
@injectable()
export class MetaParameterEditController implements IInitializableController, IDestructibleController {
isLoading = true;
user: AdminUserInfo | null = null;
get isDisabled() {
return this.usersResource.isDataLoading(this.userId);
}
userId!: string;
readonly error = new GQLErrorCatcher();
constructor(private readonly notificationService: NotificationService, private readonly usersResource: UsersResource) {
makeObservable(this, {
isLoading: observable,
user: observable,
isDisabled: computed,
});
this.updateUser = this.updateUser.bind(this);
}
async init(id: string): Promise<void> {
this.userId = id;
await this.loadUser();
this.usersResource.onItemUpdate.addHandler(this.updateUser);
}
destruct(): void {
this.usersResource.onItemUpdate.removeHandler(this.updateUser);
}
private async loadUser() {
try {
// we create a copy to protect the current value from mutation
await this.usersResource.load(this.userId);
await this.updateUser(this.userId);
} catch (exception: any) {
this.notificationService.logException(exception, `Can't load user ${this.userId}`);
}
}
private async updateUser(key: ResourceKey<string>) {
if (!ResourceKeyUtils.includes(key, this.userId)) {
return;
}
this.user = JSON.parse(JSON.stringify(await this.usersResource.load(this.userId)));
}
}
@@ -1,137 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css, use } from 'reshadow';
import type { IAdministrationItemSubItem } from '@cloudbeaver/core-administration';
import { UserMetaParametersResource } from '@cloudbeaver/core-authentication';
import {
BASE_LAYOUT_GRID_STYLES,
Loader,
Table,
TableBody,
TableColumnHeader,
TableHeader,
TableSelect,
ToolsAction,
ToolsPanel,
useResource,
useStyles,
useTranslate,
} from '@cloudbeaver/core-blocks';
import { useController, useService } from '@cloudbeaver/core-di';
import { CreateMetaParameter } from './CreateMetaParameter';
import { CreateMetaParameterService } from './CreateMetaParameterService';
import { MetadataParam } from './MetadataParam';
import { MetaParametersController } from './MetaParametersController';
const layoutStyles = css`
layout-grid {
width: 100%;
overflow: auto;
}
layout-grid-inner {
min-height: 100%;
}
layout-grid-cell {
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
position: relative;
border: solid 1px;
}
`;
const styles = css`
Table {
width: 100%;
}
`;
interface Props {
sub?: IAdministrationItemSubItem;
param?: string | null;
}
export const MetaParameters = observer<Props>(function MetaParameters({ sub, param }) {
const translate = useTranslate();
const controller = useController(MetaParametersController);
const createMetaParameterService = useService(CreateMetaParameterService);
const userMetaParametersResource = useResource(MetaParameters, UserMetaParametersResource, undefined);
const keys = userMetaParametersResource.data.map(metadata => metadata.id);
const isLocalProviderAvailable = controller.isLocalProviderAvailable;
return styled(useStyles(BASE_LAYOUT_GRID_STYLES, styles, layoutStyles))(
<layout-grid>
<layout-grid-inner>
<layout-grid-cell data-span="12">
<ToolsPanel>
{isLocalProviderAvailable && (
<ToolsAction
title={translate('authentication_administration_tools_add_tooltip')}
icon="add"
viewBox="0 0 24 24"
disabled={(sub && !!createMetaParameterService.user) || true}
onClick={createMetaParameterService.create}
>
{translate('ui_add')}
</ToolsAction>
)}
<ToolsAction
title={translate('authentication_administration_tools_refresh_tooltip')}
icon="refresh"
viewBox="0 0 24 24"
onClick={userMetaParametersResource.reload}
>
{translate('ui_refresh')}
</ToolsAction>
{isLocalProviderAvailable && (
<ToolsAction
title={translate('authentication_administration_tools_delete_tooltip')}
icon="trash"
viewBox="0 0 24 24"
disabled={!controller.itemsSelected || true}
onClick={controller.delete}
>
{translate('ui_delete')}
</ToolsAction>
)}
</ToolsPanel>
</layout-grid-cell>
{param === 'create' && createMetaParameterService.user && (
<layout-grid-cell data-span="12">
<CreateMetaParameter user={createMetaParameterService.user} onCancel={createMetaParameterService.cancelCreate} />
</layout-grid-cell>
)}
<layout-grid-cell data-span="12">
<Table keys={keys} selectedItems={controller.selectedItems} expandedItems={controller.expandedItems} size="big">
<TableHeader fixed>
{isLocalProviderAvailable && (
<TableColumnHeader min flex centerContent>
<TableSelect />
</TableColumnHeader>
)}
<TableColumnHeader min />
<TableColumnHeader>{translate('authentication_user_name')}</TableColumnHeader>
<TableColumnHeader>{translate('authentication_user_team')}</TableColumnHeader>
<TableColumnHeader />
</TableHeader>
<TableBody>
{userMetaParametersResource.data.map(param => (
<MetadataParam key={param.id} param={param} selectable={isLocalProviderAvailable} />
))}
</TableBody>
</Table>
<Loader state={userMetaParametersResource} overlay />
</layout-grid-cell>
</layout-grid-inner>
</layout-grid>,
);
});
@@ -1,124 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 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 { computed, makeObservable, observable } from 'mobx';
import { AdminUser, AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, compareUsers, UsersResource } from '@cloudbeaver/core-authentication';
import { IInitializableController, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { CachedMapAllKey, GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk';
@injectable()
export class MetaParametersController implements IInitializableController {
isDeleting = false;
readonly selectedItems = observable<string, boolean>(new Map());
readonly expandedItems = observable<string, boolean>(new Map());
readonly error = new GQLErrorCatcher();
get users(): AdminUser[] {
return Array.from(this.usersResource.data.values()).sort(compareUsers);
}
get isProvidersLoading(): boolean {
return this.authProvidersResource.isLoading();
}
get isLocalProviderAvailable(): boolean {
return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID);
}
get isLoading(): boolean {
return this.usersResource.isLoading() || this.isDeleting;
}
get itemsSelected(): boolean {
return Array.from(this.selectedItems.values()).some(v => v);
}
constructor(
private readonly notificationService: NotificationService,
private readonly authProvidersResource: AuthProvidersResource,
private readonly usersResource: UsersResource,
private readonly commonDialogService: CommonDialogService,
private readonly localizationService: LocalizationService,
) {
makeObservable(this, {
isDeleting: observable,
users: computed,
itemsSelected: computed,
});
}
init(): void {
this.authProvidersResource.load(CachedMapAllKey);
}
update = async () => {
try {
this.usersResource.markOutdated();
this.notificationService.logSuccess({ title: 'authentication_administration_tools_refresh_success' });
} catch (exception: any) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Users update failed');
}
}
};
delete = async () => {
if (this.isDeleting) {
return;
}
const deletionList = Array.from(this.selectedItems)
.filter(([_, value]) => value)
.map(([userId]) => userId);
if (deletionList.length === 0) {
return;
}
const userNames = deletionList.map(name => `"${name}"`).join(', ');
const message = `${this.localizationService.translate(
'authentication_administration_users_delete_confirmation',
)}${userNames}. ${this.localizationService.translate('ui_are_you_sure')}`;
const result = await this.commonDialogService.open(ConfirmationDialogDelete, {
title: 'ui_data_delete_confirmation',
message,
confirmActionText: 'ui_delete',
});
if (result === DialogueStateResult.Rejected) {
return;
}
this.isDeleting = true;
try {
await this.usersResource.delete(resourceKeyList(deletionList));
this.selectedItems.clear();
for (const id of deletionList) {
this.expandedItems.delete(id);
}
} catch (exception: any) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'User delete failed');
}
} finally {
this.isDeleting = false;
}
};
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
};
}
@@ -1,41 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { TableColumnValue, TableItem, TableItemExpand, TableItemSelect } from '@cloudbeaver/core-blocks';
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
const styles = css`
TableColumnValue[expand] {
cursor: pointer;
}
`;
interface Props {
param: ObjectPropertyInfo;
selectable?: boolean;
}
export const MetadataParam = observer<Props>(function MetadataParam({ param, selectable }) {
return styled(styles)(
<TableItem item={param.id} selectDisabled={!selectable}>
{selectable && (
<TableColumnValue centerContent flex>
<TableItemSelect />
</TableColumnValue>
)}
<TableColumnValue centerContent flex expand>
<TableItemExpand />
</TableColumnValue>
<TableColumnValue expand>{param.id}</TableColumnValue>
<TableColumnValue>{param.description}</TableColumnValue>
<TableColumnValue>{}</TableColumnValue>
</TableItem>,
);
});
@@ -1,44 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
import { PlaceholderComponent, StaticImage } from '@cloudbeaver/core-blocks';
import type { ObjectOrigin } from '@cloudbeaver/core-sdk';
import type { IUserDetailsInfoProps } from '../../UsersAdministrationService';
const USER_DETAILS_STYLES = css`
StaticImage {
width: 24px;
height: 24px;
}
`;
interface IOriginIconProps {
origin: ObjectOrigin;
}
export const OriginIcon = observer<IOriginIconProps>(function Origin({ origin }) {
const isLocal = origin.type === AUTH_PROVIDER_LOCAL_ID;
const icon = isLocal ? '/icons/local_connection.svg' : origin.icon;
const title = isLocal ? 'Local user' : origin.displayName;
return styled(USER_DETAILS_STYLES)(<StaticImage key={origin.type + origin.subType} icon={icon} title={title} />);
});
export const Origin: PlaceholderComponent<IUserDetailsInfoProps> = observer(function Origin({ user }) {
return (
<>
{user.origins.map(origin => (
<OriginIcon key={origin.type + origin.subType} origin={origin} />
))}
</>
);
});
@@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite';
import { useRef } from 'react';
import styled, { css } from 'reshadow';
import { ColoredContainer, Group, InputField, SubmittingForm, Textarea, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { ColoredContainer, Form, Group, InputField, Textarea, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
@@ -18,7 +18,7 @@ import { Permissions } from './Permissions';
import { TeamMetaParameters } from './TeamMetaParameters';
const styles = css`
SubmittingForm {
Form {
flex: 1;
overflow: auto;
}
@@ -32,7 +32,7 @@ export const TeamOptions: TabContainerPanelComponent<ITeamFormProps> = observer(
const edit = state.mode === 'edit';
return styled(style)(
<SubmittingForm ref={formRef}>
<Form ref={formRef}>
<ColoredContainer parent gap overflow>
<Group small gap>
<InputField name="teamId" state={state.config} readOnly={state.readonly || edit} disabled={state.disabled} required tiny fill>
@@ -48,6 +48,6 @@ export const TeamOptions: TabContainerPanelComponent<ITeamFormProps> = observer(
{!serverConfigResource.resource.distributed && <Permissions state={state} />}
<TeamMetaParameters state={state} />
</ColoredContainer>
</SubmittingForm>,
</Form>,
);
});
@@ -84,7 +84,7 @@ export function useTeamsTable(): Readonly<State> {
await resource.resource.deleteTeam(resourceKeyList(deletionList), { force: true });
this.tableState.unselect();
this.tableState.unexpand(deletionList);
this.tableState.collapse(deletionList);
} catch (exception: any) {
notificationService.logException(exception, 'Teams delete Error');
} finally {
@@ -0,0 +1,45 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
.bar {
composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global;
position: relative;
display: flex;
margin-bottom: 16px;
&:before {
content: '';
position: absolute;
bottom: 0;
width: 100%;
border-bottom: solid 2px;
border-color: inherit;
}
}
.tabList {
display: flex;
position: relative;
flex-shrink: 0;
align-items: center;
}
.contentBox {
composes: theme-background-secondary theme-text-on-secondary theme-border-color-background from global;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
.submittingForm {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
}

Some files were not shown because too many files have changed in this diff Show More