CB-4767 generate domains using deployment manager (#2515)

* CB-4767 fix: group close styles

* CB-4767 chore: log only errors and warnings for webpack dev server

* CB-4767 chore: deprecate useController

* CB-4767 feat: an option to throw exceptions with useAutoLoad

* CB-4767 fix: rethrow exception from interrupted load of resource

* CB-4767 fix: replace useController with useResource

* CB-4767 feat: add schema validation for MetadataMap

* CB-4767 fix: wrap administration page with TabPanel

* CB-4767 feat: support for contents style in TabPanel

* CB-4767 feat: support for schema for useTabLocalState

* CB-4767 feat: support async form submitting

* CB-4767 feat: add skip localization

* CB-4767 fix: types

* CB-4767 feat: support administration tab state

* CB-4767 feat: form part getter API

* CB-4767 feat: async status read API

* CB-4770 clear all auth tokens when new domain is generated

* CB-4770 save common config api

* CB-4770 fixes after review

* CB-4770 fix sql query

* CB-4770 delete certificate on certbot and get workspace id

---------

Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: Ainur <ainur.iagudin@dbeaver.com>
This commit is contained in:
Alexey
2024-04-16 17:33:39 +00:00
committed by GitHub
co-authored by kseniaguzeeva Daria Marutkina Ainur
parent 3e71a7a6ce
commit 91d83fdac8
38 changed files with 247 additions and 114 deletions
@@ -48,6 +48,7 @@ import org.jkiss.dbeaver.model.security.SMObjectType;
import org.jkiss.dbeaver.model.websocket.event.WSEventController;
import org.jkiss.dbeaver.model.websocket.event.WSServerConfigurationChangedEvent;
import org.jkiss.dbeaver.registry.BaseApplicationImpl;
import org.jkiss.dbeaver.registry.BaseWorkspaceImpl;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.dbeaver.runtime.ui.DBPPlatformUI;
import org.jkiss.dbeaver.utils.GeneralUtils;
@@ -321,6 +322,8 @@ public abstract class CBApplication<T extends CBServerConfig> extends BaseWebApp
System.setSecurityManager(new SecurityManager());
}
saveWorkspaceId();
eventController.scheduleCheckJob();
runWebServer();
@@ -330,6 +333,16 @@ public abstract class CBApplication<T extends CBServerConfig> extends BaseWebApp
return;
}
private void saveWorkspaceId() {
final Path metadataFolder = GeneralUtils.getMetadataFolder();
Properties props = BaseWorkspaceImpl.readWorkspaceInfo(metadataFolder);
String workspaceIdProperty = "workspace-id";
if (!props.contains(workspaceIdProperty)) {
props.setProperty(workspaceIdProperty, CBPlatform.getInstance().getWorkspace().getWorkspaceId());
BaseWorkspaceImpl.writeWorkspaceInfo(metadataFolder, props);
}
}
protected void initializeAdditionalConfiguration() {
}
@@ -25,6 +25,7 @@ import io.cloudbeaver.service.security.PasswordPolicyConfiguration;
import io.cloudbeaver.service.security.SMControllerConfiguration;
import io.cloudbeaver.utils.WebAppUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.ModelPreferences;
@@ -363,6 +364,11 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
}
}
public void updateServerUrl(@NotNull SMCredentialsProvider credentialsProvider, @Nullable String newPublicUrl) throws DBException {
getServerConfiguration().setServerURL(newPublicUrl);
}
protected Map<String, Object> collectConfigurationProperties(
String newServerName,
String newServerURL,
@@ -1815,7 +1815,17 @@ public class CBEmbeddedSecurityController<T extends WebAuthApplication>
}
}
private void invalidateAllUserTokens(String userId) throws DBCException {
@Override
public void invalidateAllTokens() throws DBCException {
try (Connection dbCon = database.openConnection()) {
JDBCUtils.executeStatement(
dbCon, database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_TOKEN"));
} catch (SQLException e) {
throw new DBCException("Session invalidation failed", e);
}
}
private void invalidateAllUserTokens(@NotNull String userId) throws DBCException {
try (Connection dbCon = database.openConnection()) {
JDBCUtils.executeStatement(
dbCon, database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE USER_ID=?"), userId);
@@ -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<string, any>;
readonly info: IAdministrationScreenInfo;
readonly itemState: MetadataMap<string, any>;
get isAdministrationPageActive(): boolean {
return this.isAdministrationRouteActive(this.screenService.routerService.state.name);
@@ -64,6 +56,8 @@ export class AdministrationScreenService {
readonly ensurePermissions: IExecutor;
readonly activationEvent: IExecutor<boolean>;
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, 'itemsStateSync'>(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<T>(name: string): T | undefined;
getItemState<T>(name: string, defaultState: () => T, update?: boolean, validate?: (state: T) => boolean): T;
getItemState<T>(name: string, defaultState?: () => T, update?: boolean, validate?: (state: T) => boolean): T | undefined {
getItemState<T>(name: string, defaultState: DefaultValueGetter<string, T>, schema?: schema.AnyZodObject): T;
getItemState<T>(name: string, defaultState?: DefaultValueGetter<string, T>, 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<boolean> {
if (!this.isAdministrationRouteActive(state.name)) {
return false;
@@ -307,5 +301,6 @@ function getDefaultAdministrationScreenInfo(): IAdministrationScreenInfo {
version: GlobalConstants.version || '',
serverVersion: '',
configurationMode: false,
itemsState: [],
};
}
@@ -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<typeof ADMINISTRATION_SCREEN_STATE_SCHEMA>;
@@ -5,5 +5,6 @@
display: flex;
position: absolute;
right: 24px;
z-index: 1;
margin-right: 0 !important;
}
@@ -38,14 +38,12 @@ export const Form = forwardRef<HTMLFormElement, FormDetailedProps>(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,
@@ -21,7 +21,7 @@ export interface IChangeData {
export interface IFormContext {
ref: HTMLFormElement | null;
onValidate: SyncExecutor;
onSubmit: SyncExecutor<SubmitEvent | undefined>;
onSubmit: IExecutor<SubmitEvent | undefined>;
onChange: IExecutor<IChangeData>;
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<void>;
}
export const FormContext = createContext<IFormContext | null>(null);
@@ -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> | void;
onChange?: FormChangeHandler;
}
export function useForm(options?: IOptions): IFormContext {
let parentForm = useContext(FormContext);
const [submittingExecutor] = useState(() => new SyncExecutor<SubmitEvent | undefined>());
const [submittingExecutor] = useState(() => new Executor<SubmitEvent | undefined>());
const [validationExecutor] = useState(() => new SyncExecutor());
const [changeExecutor] = useState(() => new Executor<IChangeData>());
@@ -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);
}
}
},
@@ -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<ILoadableState>, enabled = true, lazy = false) {
export function useAutoLoad(
component: { name: string },
state: ILoadableState | ReadonlyArray<ILoadableState>,
enabled = true,
lazy = false,
throwExceptions = false,
) {
const [loadFunctionName] = useState(`${component.name}.useAutoLoad(...)` as const);
if (!Array.isArray(state)) {
state = [state] as ReadonlyArray<ILoadableState>;
@@ -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]();
});
@@ -61,6 +61,9 @@ module.exports = (env, argv) => {
removeEmptyChunks: false,
splitChunks: false,
},
infrastructureLogging: {
level: 'warn',
},
devServer: {
allowedHosts: 'all',
host: 'localhost',
@@ -7,4 +7,7 @@
*/
import type { IDataContextProvider } from './IDataContextProvider';
export type DataContextGetter<T> = (provider: IDataContextProvider) => T;
export type DataContextGetter<T> = {
(provider: IDataContextProvider): T;
id: string;
};
@@ -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<T>(
return defaultValue?.(context) as T;
},
};
return obj[name];
Object.defineProperty(obj[name], 'id', { value: uuid() });
return obj[name] as DataContextGetter<T>;
}
@@ -14,7 +14,13 @@ import type { ExtractInitArgs, IDestructibleController, IInitializableController
* @deprecated use hooks instead
*/
export function useController<T extends IInitializableController>(ctor: IServiceConstructor<T>, ...args: ExtractInitArgs<T>): T;
/**
* @deprecated use hooks instead
*/
export function useController<T>(ctor: IServiceConstructor<T>): T;
/**
* @deprecated use hooks instead
*/
export function useController<T>(ctor: IServiceConstructor<T>, ...args: any[]): T {
const app = useContext(appContext);
const controllerRef = useRef<T>();
@@ -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'],
@@ -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'],
@@ -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} секунд'],
@@ -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'],
@@ -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;
}
@@ -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<AsyncTaskInfo>;
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<void> {
+4 -1
View File
@@ -23,7 +23,10 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
protected loaded: boolean;
protected loading: boolean;
constructor(protected readonly formState: IFormState<TFormState>, initialState: TPartState) {
constructor(
protected readonly formState: IFormState<TFormState>,
initialState: TPartState,
) {
this.initialState = initialState;
this.state = toJS(this.initialState);
+13 -2
View File
@@ -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<TState> implements IFormState<TState> {
mode: FormMode;
parts: MetadataMap<string, any>;
parts: MetadataMap<string, IFormPart<any>>;
state: TState;
isDisabled: boolean;
@@ -122,6 +123,10 @@ export class FormState<TState> implements IFormState<TState> {
return Array.from(this.parts.values()).some(part => part.isChanged());
}
getPart<T extends IFormPart<any>>(getter: DataContextGetter<T>): T {
return this.parts.get(getter.id, () => this.dataContext.get(getter)) as T;
}
async load(refresh?: boolean): Promise<void> {
if (this.promise !== null) {
return this.promise;
@@ -178,6 +183,12 @@ export class FormState<TState> implements IFormState<TState> {
}
}
reset(): void {
for (const part of this.parts.values()) {
part.reset();
}
}
setMode(mode: FormMode): this {
this.mode = mode;
return this;
@@ -8,8 +8,8 @@
import type { ILoadableState } from '@cloudbeaver/core-utils';
export interface IFormPart<TState> extends ILoadableState {
state: TState;
initialState: TState;
readonly state: TState;
readonly initialState: TState;
isChanged(): boolean;
@@ -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<TState> extends ILoadableState {
readonly id: string;
@@ -41,6 +42,8 @@ export interface IFormState<TState> extends ILoadableState {
setException(exception: Error | (Error | null)[] | null): this;
setState(state: TState): this;
getPart<T extends IFormPart<any>>(getter: DataContextGetter<T>): T;
isLoading(): boolean;
isLoaded(): boolean;
isError(): boolean;
@@ -51,5 +54,6 @@ export interface IFormState<TState> extends ILoadableState {
load(): Promise<void>;
reload(): Promise<void>;
save(): Promise<boolean>;
reset(): void;
cancel(): void;
}
@@ -7,11 +7,17 @@
*/
.tabPanel {
flex: 1;
display: flex;
overflow: hidden;
outline: none;
position: relative;
&:not(.contents) {
flex: 1;
display: flex;
overflow: hidden;
}
}
.contents {
display: contents;
}
.contents {
@@ -9,6 +9,7 @@ import type { TabStateReturn } from 'reakit/Tab';
export interface TabPanelProps {
tabId: string;
contents?: boolean;
className?: string;
contents?: boolean;
children?: React.ReactNode | ((state: TabStateReturn) => React.ReactNode);
@@ -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<TProps = void, TOptions extends Record<string, a
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;
getTabState: <T>(
state: MetadataMap<string, any>,
tabId: string,
props: TProps,
valueGetter?: MetadataValueGetter<string, T>,
schema?: schema.AnyZodObject,
) => T;
getDisplayed: (props?: TProps) => Array<ITabInfo<TProps, TOptions>>;
getIdList: (props?: TProps) => string[];
}
@@ -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<TProps = void, TOptions extends Record<string, any> =
return this.tabInfoMap.get(tabId);
}
getTabState<T>(state: MetadataMap<string, any>, tabId: string, props: TProps, valueGetter?: MetadataValueGetter<string, T>): T {
getTabState<T>(
state: MetadataMap<string, any>,
tabId: string,
props: TProps,
valueGetter?: MetadataValueGetter<string, T>,
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<ITabInfo<TProps, TOptions>> {
@@ -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<T = Record<string, any>> {
context: IDataContext;
canClose: (tabId: string) => boolean;
getTabInfo: (tabId: string) => ITabInfo<T> | undefined;
getTabState: <T>(tabId: string, valueGetter?: MetadataValueGetter<string, T>) => T;
getLocalState: <T>(tabId: string, valueGetter?: MetadataValueGetter<string, T>) => T;
getTabState: <T>(tabId: string, valueGetter?: MetadataValueGetter<string, T>, schema?: schema.AnyZodObject) => T;
getLocalState: <T>(tabId: string, valueGetter?: MetadataValueGetter<string, T>, schema?: schema.AnyZodObject) => T;
open: (tabId: string) => Promise<void>;
close: (tabId: string) => Promise<void>;
closeAll: () => Promise<void>;
@@ -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<T = Record<string, any>>({
getTabInfo(tabId: string) {
return dynamic.container?.getDisplayedTabInfo(tabId, dynamic.props);
},
getTabState(tabId: string, valueGetter?: MetadataValueGetter<string, any>) {
return dynamic.container?.getTabState(dynamic.tabsState, tabId, dynamic.props, valueGetter);
getTabState(tabId: string, valueGetter?: MetadataValueGetter<string, any>, schema?: schema.AnyZodObject) {
return dynamic.container?.getTabState(dynamic.tabsState, tabId, dynamic.props, valueGetter, schema);
},
getLocalState(tabId: string, valueGetter?: MetadataValueGetter<string, any>) {
return dynamic.tabsState.get(tabId, valueGetter);
getLocalState(tabId: string, valueGetter?: MetadataValueGetter<string, any>, schema?: schema.AnyZodObject) {
return dynamic.tabsState.get(tabId, valueGetter, schema);
},
async open(tabId: string) {
await openExecutor.execute({
@@ -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<T>(valueGetter?: MetadataValueGetter<string, T>): T {
export function useTabLocalState<T>(valueGetter?: MetadataValueGetter<string, T>, 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);
}
@@ -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);
}
+15 -2
View File
@@ -7,6 +7,7 @@
*/
import { action, makeAutoObservable, observable } from 'mobx';
import type { schema } from './schema';
import { TempMap } from './TempMap';
export type MetadataValueGetter<TKey, TValue> = (key: TKey, metadata: MetadataMap<TKey, any>) => TValue;
@@ -74,8 +75,19 @@ export class MetadataMap<TKey, TValue> implements Map<TKey, TValue> {
return this;
}
get(key: TKey, defaultValue?: DefaultValueGetter<TKey, TValue>): TValue {
if (!this.temp.has(key)) {
get(key: TKey, defaultValue?: DefaultValueGetter<TKey, TValue>, 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<TKey, TValue> implements Map<TKey, TValue> {
const value = provider(key, this);
this.temp.set(key, observable(value as any));
}
return this.temp.get(key)!;
}
@@ -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<React.PropsWithChildren<Props>>(function
}) {
const styles = useS(style);
const contentRef = useRef<HTMLDivElement>(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<React.PropsWithChildren<Props>>(function
}, [activeScreen?.item]);
return (
<CaptureView view={administrationViewService} className={s(styles, { administration: true, captureView: true })}>
<CaptureView view={administrationViewService} className={s(styles, { captureView: true })}>
<AdministrationCaptureViewContext />
<TabsState currentTabId={activeScreen?.item} orientation="vertical">
<TabsState currentTabId={activeScreen?.item} localState={administrationScreenService.itemState} orientation="vertical">
<SContext registry={tabsRegistry}>
<TabList aria-label="Administration items">
{items.map(item => (
@@ -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<Props>(function ItemContent({ activeScreen,
const Component = sub.getComponent ? sub.getComponent() : item.getContentComponent();
return (
<Loader suspense>
<TabPanel tabId={activeScreen.item} contents>
<Component item={item} sub={sub} param={activeScreen.param} configurationWizard={configurationWizard} />
</Loader>
</TabPanel>
);
}
}
@@ -50,8 +51,8 @@ export const ItemContent = observer<Props>(function ItemContent({ activeScreen,
const Component = item.getContentComponent();
return (
<Loader suspense>
<TabPanel tabId={activeScreen.item} contents>
<Component item={item} configurationWizard={configurationWizard} />
</Loader>
</TabPanel>
);
});
@@ -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<HTMLDivElement>({
focusFirstChild: true,
});
@@ -115,15 +115,8 @@ export class ServerConfigurationService {
async loadConfig(reset = false): Promise<void> {
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();
@@ -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),
});
}
}