diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 09c8843256..b0e247f2cc 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -13,22 +13,15 @@ import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { EAdminPermission, PermissionsService, ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; import { RouterState, ScreenService } from '@cloudbeaver/core-routing'; import { StorageService } from '@cloudbeaver/core-storage'; -import { GlobalConstants } from '@cloudbeaver/core-utils'; +import { DefaultValueGetter, GlobalConstants, MetadataMap, schema } from '@cloudbeaver/core-utils'; import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService'; import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute'; import type { IRouteParams } from '../AdministrationItem/IRouteParams'; +import { ADMINISTRATION_SCREEN_STATE_SCHEMA, type IAdministrationScreenInfo } from './IAdministrationScreenState'; -const ADMINISTRATION_ITEMS_STATE = 'administration_items_state'; const ADMINISTRATION_INFO = 'administration_info'; -interface IAdministrationScreenInfo { - workspaceId: string; - version: string; - serverVersion: string; - configurationMode: boolean; -} - @injectable() export class AdministrationScreenService { static screenName = 'administration'; @@ -41,9 +34,8 @@ export class AdministrationScreenService { static setupItemSubRouteName = 'setup.item.sub'; static setupItemSubParamRouteName = 'setup.item.sub.param'; - info: IAdministrationScreenInfo; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - itemState: Map; + readonly info: IAdministrationScreenInfo; + readonly itemState: MetadataMap; get isAdministrationPageActive(): boolean { return this.isAdministrationRouteActive(this.screenService.routerService.state.name); @@ -64,6 +56,8 @@ export class AdministrationScreenService { readonly ensurePermissions: IExecutor; readonly activationEvent: IExecutor; + private itemsStateSync: Array<[string, any]>; + constructor( private readonly permissionsResource: SessionPermissionsResource, private readonly permissionsService: PermissionsService, @@ -74,18 +68,46 @@ export class AdministrationScreenService { private readonly notificationService: NotificationService, ) { this.info = getDefaultAdministrationScreenInfo(); - this.itemState = new Map(); + this.itemState = new MetadataMap(); this.activationEvent = new Executor(); this.ensurePermissions = new Executor(); + this.itemsStateSync = []; - makeObservable(this, { + makeObservable(this, { info: observable, - itemState: observable, + itemsStateSync: observable, activeScreen: computed, }); - this.storageService.registerSettings(ADMINISTRATION_ITEMS_STATE, this.itemState, () => new Map()); - this.storageService.registerSettings(ADMINISTRATION_INFO, this.info, getDefaultAdministrationScreenInfo); + this.storageService.registerSettings( + ADMINISTRATION_INFO, + this.info, + getDefaultAdministrationScreenInfo, + data => { + const parsed = ADMINISTRATION_SCREEN_STATE_SCHEMA.safeParse(data); + + if ( + !parsed.success || + parsed.data.workspaceId !== this.serverConfigResource.workspaceId || + parsed.data.configurationMode !== this.isConfigurationMode || + parsed.data.serverVersion !== this.serverConfigResource.serverVersion || + parsed.data.version !== GlobalConstants.version + ) { + return { + workspaceId: this.serverConfigResource.workspaceId, + configurationMode: this.isConfigurationMode, + serverVersion: this.serverConfigResource.serverVersion, + version: GlobalConstants.version || '', + itemsState: observable([], { deep: true }), + }; + } + + return { ...parsed.data, itemsState: observable(parsed.data.itemsState, { deep: true }) }; + }, + () => { + this.itemState.sync(this.itemsStateSync); + }, + ); this.permissionsResource.onDataUpdate.addPostHandler(() => { this.checkPermissions(this.screenService.routerService.state); }); @@ -153,26 +175,13 @@ export class AdministrationScreenService { } getItemState(name: string): T | undefined; - getItemState(name: string, defaultState: () => T, update?: boolean, validate?: (state: T) => boolean): T; - getItemState(name: string, defaultState?: () => T, update?: boolean, validate?: (state: T) => boolean): T | undefined { + getItemState(name: string, defaultState: DefaultValueGetter, schema?: schema.AnyZodObject): T; + getItemState(name: string, defaultState?: DefaultValueGetter, schema?: schema.AnyZodObject): T | undefined { if (!this.serverConfigResource.isLoaded()) { throw new Error('Administration screen getItemState can be used only after server configuration loaded'); } - this.validateState(); - - if (defaultState) { - if (!this.itemState.has(name) || update) { - this.itemState.set(name, defaultState()); - } else if (validate) { - const state = this.itemState.get(name)!; - - if (!validate(state)) { - this.itemState.set(name, defaultState()); - } - } - } - return this.itemState.get(name); + return this.itemState.get(name, defaultState, schema); } clearItemsState(): void { @@ -246,21 +255,6 @@ export class AdministrationScreenService { } } - private validateState() { - if ( - this.info.workspaceId !== this.serverConfigResource.workspaceId || - this.info.configurationMode !== this.isConfigurationMode || - this.info.serverVersion !== this.serverConfigResource.serverVersion || - this.info.version !== GlobalConstants.version - ) { - this.clearItemsState(); - this.info.workspaceId = this.serverConfigResource.workspaceId; - this.info.configurationMode = this.isConfigurationMode; - this.info.serverVersion = this.serverConfigResource.serverVersion; - this.info.version = GlobalConstants.version || ''; - } - } - private async checkPermissions(state: RouterState): Promise { if (!this.isAdministrationRouteActive(state.name)) { return false; @@ -307,5 +301,6 @@ function getDefaultAdministrationScreenInfo(): IAdministrationScreenInfo { version: GlobalConstants.version || '', serverVersion: '', configurationMode: false, + itemsState: [], }; } diff --git a/webapp/packages/core-administration/src/AdministrationScreen/IAdministrationScreenState.ts b/webapp/packages/core-administration/src/AdministrationScreen/IAdministrationScreenState.ts new file mode 100644 index 0000000000..b8408fbe9e --- /dev/null +++ b/webapp/packages/core-administration/src/AdministrationScreen/IAdministrationScreenState.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { schema } from '@cloudbeaver/core-utils'; + +export const ADMINISTRATION_SCREEN_STATE_SCHEMA = schema.object({ + workspaceId: schema.string(), + version: schema.string(), + serverVersion: schema.string(), + configurationMode: schema.boolean(), + itemsState: schema.array(schema.tuple([schema.string(), schema.any()])), +}); + +export type IAdministrationScreenInfo = schema.infer; diff --git a/webapp/packages/core-blocks/src/Containers/GroupClose.m.css b/webapp/packages/core-blocks/src/Containers/GroupClose.m.css index 9a7d30a9e3..31102daba0 100644 --- a/webapp/packages/core-blocks/src/Containers/GroupClose.m.css +++ b/webapp/packages/core-blocks/src/Containers/GroupClose.m.css @@ -5,5 +5,6 @@ display: flex; position: absolute; right: 24px; + z-index: 1; margin-right: 0 !important; } diff --git a/webapp/packages/core-blocks/src/FormControls/Form.tsx b/webapp/packages/core-blocks/src/FormControls/Form.tsx index ffbbe061b4..d1ad986252 100644 --- a/webapp/packages/core-blocks/src/FormControls/Form.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Form.tsx @@ -33,14 +33,12 @@ export const Form = forwardRef(function Form const formContext = useForm({ disableEnterSubmit, parent: context, - onSubmit(event) { - const result = onSubmit?.(event); - - if (result instanceof Promise) { + async onSubmit(event) { + try { setDisabledLocal(true); - result.finally(() => { - setDisabledLocal(false); - }); + await onSubmit?.(event); + } finally { + setDisabledLocal(false); } }, onChange, diff --git a/webapp/packages/core-blocks/src/FormControls/FormContext.ts b/webapp/packages/core-blocks/src/FormControls/FormContext.ts index ead113f04c..a676cecee9 100644 --- a/webapp/packages/core-blocks/src/FormControls/FormContext.ts +++ b/webapp/packages/core-blocks/src/FormControls/FormContext.ts @@ -21,7 +21,7 @@ export interface IChangeData { export interface IFormContext { ref: HTMLFormElement | null; onValidate: SyncExecutor; - onSubmit: SyncExecutor; + onSubmit: IExecutor; onChange: IExecutor; parent: IFormContext | null; disableEnterSubmit: boolean; @@ -30,7 +30,7 @@ export interface IFormContext { keyDown: KeyHandler; validate: () => boolean; reportValidity: () => boolean; - submit: (event?: SubmitEvent) => void; + submit: (event?: SubmitEvent) => Promise; } export const FormContext = createContext(null); diff --git a/webapp/packages/core-blocks/src/FormControls/useForm.ts b/webapp/packages/core-blocks/src/FormControls/useForm.ts index b0748e37b6..0466701ef0 100644 --- a/webapp/packages/core-blocks/src/FormControls/useForm.ts +++ b/webapp/packages/core-blocks/src/FormControls/useForm.ts @@ -16,13 +16,13 @@ import { FormChangeHandler, FormContext, type IChangeData, type IFormContext } f interface IOptions { parent?: IFormContext; disableEnterSubmit?: boolean; - onSubmit?: (event?: SubmitEvent | undefined) => void; + onSubmit?: (event?: SubmitEvent | undefined) => Promise | void; onChange?: FormChangeHandler; } export function useForm(options?: IOptions): IFormContext { let parentForm = useContext(FormContext); - const [submittingExecutor] = useState(() => new SyncExecutor()); + const [submittingExecutor] = useState(() => new Executor()); const [validationExecutor] = useState(() => new SyncExecutor()); const [changeExecutor] = useState(() => new Executor()); @@ -86,14 +86,14 @@ export function useForm(options?: IOptions): IFormContext { this.submit(); } }, - submit(event) { + async submit(event) { if (this.parent) { - this.parent.submit(event); + await this.parent.submit(event); } else { event?.preventDefault(); if (this.validate()) { - this.onSubmit.execute(event); + await this.onSubmit.execute(event); } } }, diff --git a/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts b/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts index fcebe417c0..b73b5985fa 100644 --- a/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts +++ b/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts @@ -7,11 +7,17 @@ */ import { useEffect, useState } from 'react'; -import type { ILoadableState } from '@cloudbeaver/core-utils'; +import { type ILoadableState, isContainsException } from '@cloudbeaver/core-utils'; import { getComputed } from '../getComputed'; -export function useAutoLoad(component: { name: string }, state: ILoadableState | ReadonlyArray, enabled = true, lazy = false) { +export function useAutoLoad( + component: { name: string }, + state: ILoadableState | ReadonlyArray, + enabled = true, + lazy = false, + throwExceptions = false, +) { const [loadFunctionName] = useState(`${component.name}.useAutoLoad(...)` as const); if (!Array.isArray(state)) { state = [state] as ReadonlyArray; @@ -50,6 +56,17 @@ export function useAutoLoad(component: { name: string }, state: ILoadableState | throw Promise.all(promises); } + if (throwExceptions) { + const exceptions = state + .map(loader => loader.exception) + .filter(isContainsException) + .flat(); + + if (exceptions.length > 0) { + throw exceptions[0]; + } + } + useEffect(() => { obj[loadFunctionName](); }); diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index 674309cf95..7150331adb 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -61,6 +61,9 @@ module.exports = (env, argv) => { removeEmptyChunks: false, splitChunks: false, }, + infrastructureLogging: { + level: 'warn', + }, devServer: { allowedHosts: 'all', host: 'localhost', diff --git a/webapp/packages/core-data-context/src/DataContext/DataContextGetter.ts b/webapp/packages/core-data-context/src/DataContext/DataContextGetter.ts index d30484b751..c946bf9fce 100644 --- a/webapp/packages/core-data-context/src/DataContext/DataContextGetter.ts +++ b/webapp/packages/core-data-context/src/DataContext/DataContextGetter.ts @@ -7,4 +7,7 @@ */ import type { IDataContextProvider } from './IDataContextProvider'; -export type DataContextGetter = (provider: IDataContextProvider) => T; +export type DataContextGetter = { + (provider: IDataContextProvider): T; + id: string; +}; diff --git a/webapp/packages/core-data-context/src/DataContext/createDataContext.ts b/webapp/packages/core-data-context/src/DataContext/createDataContext.ts index 4cdb4affdf..6f45ca86a6 100644 --- a/webapp/packages/core-data-context/src/DataContext/createDataContext.ts +++ b/webapp/packages/core-data-context/src/DataContext/createDataContext.ts @@ -5,6 +5,8 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { uuid } from '@cloudbeaver/core-utils'; + import type { DataContextGetter } from './DataContextGetter'; import type { IDataContextProvider } from './IDataContextProvider'; @@ -18,5 +20,6 @@ export function createDataContext( return defaultValue?.(context) as T; }, }; - return obj[name]; + Object.defineProperty(obj[name], 'id', { value: uuid() }); + return obj[name] as DataContextGetter; } diff --git a/webapp/packages/core-di/src/useController.ts b/webapp/packages/core-di/src/useController.ts index 13d8b91c26..fe0012ed86 100644 --- a/webapp/packages/core-di/src/useController.ts +++ b/webapp/packages/core-di/src/useController.ts @@ -14,7 +14,13 @@ import type { ExtractInitArgs, IDestructibleController, IInitializableController * @deprecated use hooks instead */ export function useController(ctor: IServiceConstructor, ...args: ExtractInitArgs): T; +/** + * @deprecated use hooks instead + */ export function useController(ctor: IServiceConstructor): T; +/** + * @deprecated use hooks instead + */ export function useController(ctor: IServiceConstructor, ...args: any[]): T { const app = useContext(appContext); const controllerRef = useRef(); diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index fb8e36b3c3..a4f6744b4c 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -18,6 +18,7 @@ export default [ ['ui_processing_save', 'Save'], ['ui_processing_saved', 'Saved'], ['ui_processing_stop', 'Stop'], + ['ui_processing_skip', 'Skip'], ['ui_second_first_form', '{arg:interval} second'], ['ui_second_second_form', '{arg:interval} seconds'], ['ui_second_third_form', '{arg:interval} seconds'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index f98cbff0c7..f3d661cafe 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -17,6 +17,7 @@ export default [ ['ui_processing_save', 'Salva'], ['ui_processing_saved', 'Saved'], ['ui_processing_stop', 'Stop'], + ['ui_processing_skip', 'Skip'], ['ui_second_first_form', '{arg:interval} second'], ['ui_second_second_form', '{arg:interval} seconds'], ['ui_second_third_form', '{arg:interval} seconds'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index a479779496..72b6777efc 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -16,6 +16,7 @@ export default [ ['ui_processing_save', 'Сохранить'], ['ui_processing_saved', 'Сохранено'], ['ui_processing_stop', 'Остановить'], + ['ui_processing_skip', 'Пропустить'], ['ui_second_first_form', '{arg:interval} секунда'], ['ui_second_second_form', '{arg:interval} секунды'], ['ui_second_third_form', '{arg:interval} секунд'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index cee7130839..faedbaa8ff 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -17,6 +17,7 @@ export default [ ['ui_processing_save', '保存'], ['ui_processing_saved', 'Saved'], ['ui_processing_stop', 'Stop'], + ['ui_processing_skip', 'Skip'], ['ui_second_first_form', '{arg:interval} second'], ['ui_second_second_form', '{arg:interval} seconds'], ['ui_second_third_form', '{arg:interval} seconds'], diff --git a/webapp/packages/core-resource/src/Resource/CachedResource.ts b/webapp/packages/core-resource/src/Resource/CachedResource.ts index 6dbf5c6a95..69070eb405 100644 --- a/webapp/packages/core-resource/src/Resource/CachedResource.ts +++ b/webapp/packages/core-resource/src/Resource/CachedResource.ts @@ -18,6 +18,7 @@ import { SyncExecutor, TaskScheduler, } from '@cloudbeaver/core-executor'; +import { getFirstException, isContainsException } from '@cloudbeaver/core-utils'; import { CachedResourceOffsetPageKey, @@ -684,6 +685,11 @@ export abstract class CachedResource< } const contexts = new ExecutionContext(key); if (!refresh) { + const exception = this.getException(key); + if (isContainsException(exception)) { + throw getFirstException(exception); + } + if (!this.isLoadable(key, include)) { return; } diff --git a/webapp/packages/core-sdk/src/AsyncTask/AsyncTask.ts b/webapp/packages/core-sdk/src/AsyncTask/AsyncTask.ts index def0152f61..ebab1e406e 100644 --- a/webapp/packages/core-sdk/src/AsyncTask/AsyncTask.ts +++ b/webapp/packages/core-sdk/src/AsyncTask/AsyncTask.ts @@ -7,6 +7,7 @@ */ import { computed, makeObservable, observable } from 'mobx'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { uuid } from '@cloudbeaver/core-utils'; import type { AsyncTaskInfo } from '../sdk'; @@ -31,6 +32,8 @@ export class AsyncTask { return this.innerPromise; } + readonly onStatusChange: ISyncExecutor; + private _cancelled: boolean; private taskInfo: AsyncTaskInfo | null; private resolve!: (value: AsyncTaskInfo) => void; @@ -49,6 +52,7 @@ export class AsyncTask { this.updatingAsync = false; this.taskInfo = null; this.initPromise = null; + this.onStatusChange = new SyncExecutor(); this.innerPromise = new Promise((resolve, reject) => { this.reject = reject; @@ -125,6 +129,7 @@ export class AsyncTask { this.resolve(info); } } + this.onStatusChange.execute(this.taskInfo); } private async cancelTask(): Promise { diff --git a/webapp/packages/core-ui/src/Form/FormPart.ts b/webapp/packages/core-ui/src/Form/FormPart.ts index 05c791c275..9329d0a527 100644 --- a/webapp/packages/core-ui/src/Form/FormPart.ts +++ b/webapp/packages/core-ui/src/Form/FormPart.ts @@ -23,7 +23,10 @@ export abstract class FormPart implements IFormPar protected loaded: boolean; protected loading: boolean; - constructor(protected readonly formState: IFormState, initialState: TPartState) { + constructor( + protected readonly formState: IFormState, + initialState: TPartState, + ) { this.initialState = initialState; this.state = toJS(this.initialState); diff --git a/webapp/packages/core-ui/src/Form/FormState.ts b/webapp/packages/core-ui/src/Form/FormState.ts index 9bcfc1ce3f..cb76a082ae 100644 --- a/webapp/packages/core-ui/src/Form/FormState.ts +++ b/webapp/packages/core-ui/src/Form/FormState.ts @@ -7,7 +7,7 @@ */ import { action, makeObservable, observable } from 'mobx'; -import { dataContextAddDIProvider, type IDataContext, TempDataContext } from '@cloudbeaver/core-data-context'; +import { dataContextAddDIProvider, DataContextGetter, type IDataContext, TempDataContext } from '@cloudbeaver/core-data-context'; import type { App } from '@cloudbeaver/core-di'; import type { ENotificationType } from '@cloudbeaver/core-events'; import { Executor, ExecutorInterrupter, IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor'; @@ -18,11 +18,12 @@ 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 { IFormPart } from './IFormPart'; import type { IFormState } from './IFormState'; export class FormState implements IFormState { mode: FormMode; - parts: MetadataMap; + parts: MetadataMap>; state: TState; isDisabled: boolean; @@ -122,6 +123,10 @@ export class FormState implements IFormState { return Array.from(this.parts.values()).some(part => part.isChanged()); } + getPart>(getter: DataContextGetter): T { + return this.parts.get(getter.id, () => this.dataContext.get(getter)) as T; + } + async load(refresh?: boolean): Promise { if (this.promise !== null) { return this.promise; @@ -178,6 +183,12 @@ export class FormState implements IFormState { } } + reset(): void { + for (const part of this.parts.values()) { + part.reset(); + } + } + setMode(mode: FormMode): this { this.mode = mode; return this; diff --git a/webapp/packages/core-ui/src/Form/IFormPart.ts b/webapp/packages/core-ui/src/Form/IFormPart.ts index 53ea7ce50c..c11b933c4a 100644 --- a/webapp/packages/core-ui/src/Form/IFormPart.ts +++ b/webapp/packages/core-ui/src/Form/IFormPart.ts @@ -8,8 +8,8 @@ import type { ILoadableState } from '@cloudbeaver/core-utils'; export interface IFormPart extends ILoadableState { - state: TState; - initialState: TState; + readonly state: TState; + readonly initialState: TState; isChanged(): boolean; diff --git a/webapp/packages/core-ui/src/Form/IFormState.ts b/webapp/packages/core-ui/src/Form/IFormState.ts index 4b469260c8..5feb223cfd 100644 --- a/webapp/packages/core-ui/src/Form/IFormState.ts +++ b/webapp/packages/core-ui/src/Form/IFormState.ts @@ -5,13 +5,14 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IDataContext } from '@cloudbeaver/core-data-context'; +import type { DataContextGetter, IDataContext } from '@cloudbeaver/core-data-context'; import type { ENotificationType } from '@cloudbeaver/core-events'; import type { IExecutor } from '@cloudbeaver/core-executor'; import type { ILoadableState, MetadataMap } from '@cloudbeaver/core-utils'; import type { FormBaseService } from './FormBaseService'; import type { FormMode } from './FormMode'; +import type { IFormPart } from './IFormPart'; export interface IFormState extends ILoadableState { readonly id: string; @@ -41,6 +42,8 @@ export interface IFormState extends ILoadableState { setException(exception: Error | (Error | null)[] | null): this; setState(state: TState): this; + getPart>(getter: DataContextGetter): T; + isLoading(): boolean; isLoaded(): boolean; isError(): boolean; @@ -51,5 +54,6 @@ export interface IFormState extends ILoadableState { load(): Promise; reload(): Promise; save(): Promise; + reset(): void; cancel(): void; } diff --git a/webapp/packages/core-ui/src/Tabs/TabPanel.m.css b/webapp/packages/core-ui/src/Tabs/TabPanel.m.css index 7cdb7c6e4c..84d80b29b8 100644 --- a/webapp/packages/core-ui/src/Tabs/TabPanel.m.css +++ b/webapp/packages/core-ui/src/Tabs/TabPanel.m.css @@ -7,9 +7,15 @@ */ .tabPanel { - flex: 1; - display: flex; - overflow: hidden; outline: none; position: relative; + + &:not(.contents) { + flex: 1; + display: flex; + overflow: hidden; + } +} +.contents { + display: contents; } diff --git a/webapp/packages/core-ui/src/Tabs/TabPanel.tsx b/webapp/packages/core-ui/src/Tabs/TabPanel.tsx index ad9f937a60..2c53a0bd82 100644 --- a/webapp/packages/core-ui/src/Tabs/TabPanel.tsx +++ b/webapp/packages/core-ui/src/Tabs/TabPanel.tsx @@ -17,7 +17,7 @@ import type { TabPanelProps } from './TabPanelProps'; import { TabsContext } from './TabsContext'; import { useTabsValidation } from './useTabsValidation'; -export const TabPanel: React.FC = observer(function TabPanel({ tabId, children, className, lazy }) { +export const TabPanel: React.FC = observer(function TabPanel({ tabId, children, contents, className, lazy }) { const tabContextState = useContext(TabsContext); const styles = useS(tabPanelStyles); @@ -44,7 +44,7 @@ export const TabPanel: React.FC = observer(function TabPanel({ ta return ( - + {renderChildren()} diff --git a/webapp/packages/core-ui/src/Tabs/TabPanelProps.ts b/webapp/packages/core-ui/src/Tabs/TabPanelProps.ts index 5ecd9cbd4d..3d0357208f 100644 --- a/webapp/packages/core-ui/src/Tabs/TabPanelProps.ts +++ b/webapp/packages/core-ui/src/Tabs/TabPanelProps.ts @@ -9,6 +9,7 @@ import type { TabStateReturn } from 'reakit/Tab'; export interface TabPanelProps { tabId: string; + contents?: boolean; className?: string; children?: React.ReactNode | ((state: TabStateReturn) => React.ReactNode); lazy?: boolean; diff --git a/webapp/packages/core-ui/src/Tabs/TabsContainer/ITabsContainer.ts b/webapp/packages/core-ui/src/Tabs/TabsContainer/ITabsContainer.ts index 457c661f21..0a20021a91 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsContainer/ITabsContainer.ts +++ b/webapp/packages/core-ui/src/Tabs/TabsContainer/ITabsContainer.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; -import type { ILoadableState, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils'; +import type { ILoadableState, MetadataMap, MetadataValueGetter, schema } from '@cloudbeaver/core-utils'; import type { TabProps } from '../Tab/TabProps'; @@ -52,7 +52,13 @@ export interface ITabsContainer boolean; getTabInfo: (tabId: string) => ITabInfo | undefined; getDisplayedTabInfo: (tabId: string, props?: TProps) => ITabInfo | undefined; - getTabState: (state: MetadataMap, tabId: string, props: TProps, valueGetter?: MetadataValueGetter) => T; + getTabState: ( + state: MetadataMap, + tabId: string, + props: TProps, + valueGetter?: MetadataValueGetter, + schema?: schema.AnyZodObject, + ) => T; getDisplayed: (props?: TProps) => Array>; getIdList: (props?: TProps) => string[]; } diff --git a/webapp/packages/core-ui/src/Tabs/TabsContainer/TabsContainer.ts b/webapp/packages/core-ui/src/Tabs/TabsContainer/TabsContainer.ts index 9c6669132d..8f21455175 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsContainer/TabsContainer.ts +++ b/webapp/packages/core-ui/src/Tabs/TabsContainer/TabsContainer.ts @@ -7,7 +7,7 @@ */ import { makeObservable, observable } from 'mobx'; -import type { MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils'; +import type { MetadataMap, MetadataValueGetter, schema } from '@cloudbeaver/core-utils'; import type { ITabInfo, ITabInfoOptions, ITabsContainer } from './ITabsContainer'; @@ -64,10 +64,16 @@ export class TabsContainer = return this.tabInfoMap.get(tabId); } - getTabState(state: MetadataMap, tabId: string, props: TProps, valueGetter?: MetadataValueGetter): T { + getTabState( + state: MetadataMap, + tabId: string, + props: TProps, + valueGetter?: MetadataValueGetter, + schema?: schema.AnyZodObject, + ): T { const tabInfo = this.getDisplayedTabInfo(tabId, props); - return state.get(tabId, valueGetter || tabInfo?.stateGetter?.(props)); + return state.get(tabId, valueGetter || tabInfo?.stateGetter?.(props), schema); } getDisplayed(props?: TProps): Array> { diff --git a/webapp/packages/core-ui/src/Tabs/TabsContext.ts b/webapp/packages/core-ui/src/Tabs/TabsContext.ts index 00be92f831..2a7f22d7de 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsContext.ts +++ b/webapp/packages/core-ui/src/Tabs/TabsContext.ts @@ -10,7 +10,7 @@ import type { TabStateReturn } from 'reakit/Tab'; import type { IDataContext } from '@cloudbeaver/core-data-context'; import type { IExecutor } from '@cloudbeaver/core-executor'; -import type { MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils'; +import type { MetadataMap, MetadataValueGetter, schema } from '@cloudbeaver/core-utils'; import type { ITabData, ITabInfo, ITabsContainer } from './TabsContainer/ITabsContainer'; @@ -30,8 +30,8 @@ export interface ITabsContext> { context: IDataContext; canClose: (tabId: string) => boolean; getTabInfo: (tabId: string) => ITabInfo | undefined; - getTabState: (tabId: string, valueGetter?: MetadataValueGetter) => T; - getLocalState: (tabId: string, valueGetter?: MetadataValueGetter) => T; + getTabState: (tabId: string, valueGetter?: MetadataValueGetter, schema?: schema.AnyZodObject) => T; + getLocalState: (tabId: string, valueGetter?: MetadataValueGetter, schema?: schema.AnyZodObject) => T; open: (tabId: string) => Promise; close: (tabId: string) => Promise; closeAll: () => Promise; diff --git a/webapp/packages/core-ui/src/Tabs/TabsState.tsx b/webapp/packages/core-ui/src/Tabs/TabsState.tsx index bb4f7d5f62..3d9f1bfd0e 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsState.tsx +++ b/webapp/packages/core-ui/src/Tabs/TabsState.tsx @@ -13,7 +13,7 @@ import { useTabState } from 'reakit/Tab'; import { useAutoLoad, useExecutor, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks'; import { useDataContext } from '@cloudbeaver/core-data-context'; import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor'; -import { isDefined, isNotNullDefined, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils'; +import { isDefined, isNotNullDefined, MetadataMap, MetadataValueGetter, schema } from '@cloudbeaver/core-utils'; import type { ITabData, ITabInfo, ITabsContainer } from './TabsContainer/ITabsContainer'; import { ITabsContext, type TabDirection, TabsContext } from './TabsContext'; @@ -164,11 +164,11 @@ export const TabsState = observer(function TabsState>({ getTabInfo(tabId: string) { return dynamic.container?.getDisplayedTabInfo(tabId, dynamic.props); }, - getTabState(tabId: string, valueGetter?: MetadataValueGetter) { - return dynamic.container?.getTabState(dynamic.tabsState, tabId, dynamic.props, valueGetter); + getTabState(tabId: string, valueGetter?: MetadataValueGetter, schema?: schema.AnyZodObject) { + return dynamic.container?.getTabState(dynamic.tabsState, tabId, dynamic.props, valueGetter, schema); }, - getLocalState(tabId: string, valueGetter?: MetadataValueGetter) { - return dynamic.tabsState.get(tabId, valueGetter); + getLocalState(tabId: string, valueGetter?: MetadataValueGetter, schema?: schema.AnyZodObject) { + return dynamic.tabsState.get(tabId, valueGetter, schema); }, async open(tabId: string) { await openExecutor.execute({ diff --git a/webapp/packages/core-ui/src/Tabs/useTabLocalState.ts b/webapp/packages/core-ui/src/Tabs/useTabLocalState.ts index 0a1eae28da..5f6e27c6d5 100644 --- a/webapp/packages/core-ui/src/Tabs/useTabLocalState.ts +++ b/webapp/packages/core-ui/src/Tabs/useTabLocalState.ts @@ -7,17 +7,17 @@ */ import { useContext } from 'react'; -import type { MetadataValueGetter } from '@cloudbeaver/core-utils'; +import type { MetadataValueGetter, schema } from '@cloudbeaver/core-utils'; import { TabContext } from './TabContext'; import { TabsContext } from './TabsContext'; -export function useTabLocalState(valueGetter?: MetadataValueGetter): T { +export function useTabLocalState(valueGetter?: MetadataValueGetter, schema?: schema.AnyZodObject): T { const state = useContext(TabsContext); const tabContext = useContext(TabContext); if (!state || !tabContext) { throw new Error('Tabs context was not provided'); } - return state.getLocalState(tabContext.tabId, valueGetter); + return state.getLocalState(tabContext.tabId, valueGetter, schema); } diff --git a/webapp/packages/core-utils/src/ILoadableState.ts b/webapp/packages/core-utils/src/ILoadableState.ts index 6e0c850710..31db51562f 100644 --- a/webapp/packages/core-utils/src/ILoadableState.ts +++ b/webapp/packages/core-utils/src/ILoadableState.ts @@ -29,7 +29,7 @@ export function isLoadableStateHasException(state: ILoadableState): boolean { return isContainsException(state.exception); } -export function isContainsException(exception?: (Error | null)[] | Error | null): boolean { +export function isContainsException(exception?: (Error | null)[] | Error | null): exception is Error[] | Error { if (Array.isArray(exception)) { return exception.some(Boolean); } diff --git a/webapp/packages/core-utils/src/MetadataMap.ts b/webapp/packages/core-utils/src/MetadataMap.ts index 70bd974af5..7f31c47c37 100644 --- a/webapp/packages/core-utils/src/MetadataMap.ts +++ b/webapp/packages/core-utils/src/MetadataMap.ts @@ -7,6 +7,7 @@ */ import { action, makeAutoObservable, observable } from 'mobx'; +import type { schema } from './schema'; import { TempMap } from './TempMap'; export type MetadataValueGetter = (key: TKey, metadata: MetadataMap) => TValue; @@ -74,8 +75,19 @@ export class MetadataMap implements Map { return this; } - get(key: TKey, defaultValue?: DefaultValueGetter): TValue { - if (!this.temp.has(key)) { + get(key: TKey, defaultValue?: DefaultValueGetter, schema?: schema.AnyZodObject): TValue { + const value = this.temp.get(key); + let invalidate = !this.temp.has(key); + + if (!invalidate && schema) { + const parsed = schema.safeParse(value); + + if (!parsed.success) { + invalidate = true; + } + } + + if (invalidate) { const provider = defaultValue || this.defaultValueGetter; if (!provider) { @@ -85,6 +97,7 @@ export class MetadataMap implements Map { const value = provider(key, this); this.temp.set(key, observable(value as any)); } + return this.temp.get(key)!; } diff --git a/webapp/packages/plugin-administration/src/Administration/Administration.tsx b/webapp/packages/plugin-administration/src/Administration/Administration.tsx index 969fc86c4f..0a453addc2 100644 --- a/webapp/packages/plugin-administration/src/Administration/Administration.tsx +++ b/webapp/packages/plugin-administration/src/Administration/Administration.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { useLayoutEffect, useRef } from 'react'; -import { AdministrationItemService, filterOnlyActive, IAdministrationItemRoute } from '@cloudbeaver/core-administration'; +import { AdministrationItemService, AdministrationScreenService, filterOnlyActive, IAdministrationItemRoute } from '@cloudbeaver/core-administration'; import { Loader, s, @@ -83,6 +83,7 @@ export const Administration = observer>(function }) { const styles = useS(style); const contentRef = useRef(null); + const administrationScreenService = useService(AdministrationScreenService); const administrationViewService = useService(AdministrationViewService); const administrationItemService = useService(AdministrationItemService); const optionsPanelService = useService(OptionsPanelService); @@ -96,9 +97,9 @@ export const Administration = observer>(function }, [activeScreen?.item]); return ( - + - + {items.map(item => ( diff --git a/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx b/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx index 9d64befaaa..b0df0ff315 100644 --- a/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx +++ b/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx @@ -10,6 +10,7 @@ import { observer } from 'mobx-react-lite'; import { AdministrationItemService, IAdministrationItemRoute } from '@cloudbeaver/core-administration'; import { Loader, TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { TabPanel } from '@cloudbeaver/core-ui'; interface Props { activeScreen: IAdministrationItemRoute | null; @@ -40,9 +41,9 @@ export const ItemContent = observer(function ItemContent({ activeScreen, const Component = sub.getComponent ? sub.getComponent() : item.getContentComponent(); return ( - + - + ); } } @@ -50,8 +51,8 @@ export const ItemContent = observer(function ItemContent({ activeScreen, const Component = item.getContentComponent(); return ( - + - + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/Finish/FinishPage.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/Finish/FinishPage.tsx index c449524a9c..9be2c34952 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/Finish/FinishPage.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/Finish/FinishPage.tsx @@ -9,13 +9,13 @@ import { observer } from 'mobx-react-lite'; import { ConfigurationWizardService } from '@cloudbeaver/core-administration'; import { Button, s, useFocus, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; +import { useService } from '@cloudbeaver/core-di'; import styles from './FinishPage.m.css'; export const FinishPage = observer(function FinishPage() { const translate = useTranslate(); - const service = useController(ConfigurationWizardService); + const service = useService(ConfigurationWizardService); const [focus] = useFocus({ focusFirstChild: true, }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 090ab61d5f..401562aef8 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -115,15 +115,8 @@ export class ServerConfigurationService { async loadConfig(reset = false): Promise { try { if (!this.stateLinked) { - this.state = this.administrationScreenService.getItemState( - 'server-configuration', - () => { - reset = true; - return serverConfigStateContext(); - }, - true, - ); - + this.state = this.administrationScreenService.getItemState('server-configuration', serverConfigStateContext); + reset = true; this.stateLinked = true; await this.serverConfigResource.load(); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts index c0e35a8811..10b00821d0 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts @@ -25,7 +25,7 @@ export class UserProfileFormInfoPartBootstrap extends Bootstrap { name: 'plugin_user_profile_info', order: 1, panel: () => UserProfileFormInfo, - stateGetter: props => () => props.formState.dataContext.get(DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART), + stateGetter: props => () => props.formState.getPart(DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART), }); } }