mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
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:
Vendored
+6
-1
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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?
|
||||
|
||||
Vendored
+20
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
+3
-6
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+4
-4
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+4
-4
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+9
@@ -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';
|
||||
-62
@@ -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>,
|
||||
);
|
||||
};
|
||||
-56
@@ -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();
|
||||
}
|
||||
}
|
||||
-131
@@ -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>,
|
||||
);
|
||||
});
|
||||
-35
@@ -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>,
|
||||
);
|
||||
});
|
||||
-32
@@ -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>,
|
||||
);
|
||||
});
|
||||
-28
@@ -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>,
|
||||
);
|
||||
});
|
||||
-48
@@ -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>,
|
||||
);
|
||||
});
|
||||
-25
@@ -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 {}
|
||||
}
|
||||
-43
@@ -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>,
|
||||
);
|
||||
});
|
||||
-65
@@ -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)));
|
||||
}
|
||||
}
|
||||
-137
@@ -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>,
|
||||
);
|
||||
});
|
||||
-124
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
-41
@@ -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>,
|
||||
);
|
||||
});
|
||||
-44
@@ -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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
+4
-4
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+45
@@ -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
Reference in New Issue
Block a user