mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 06:40:05 +08:00
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:
co-authored by
kseniaguzeeva
Daria Marutkina
Ainur
parent
3e71a7a6ce
commit
91d83fdac8
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -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,
|
||||
|
||||
+11
-1
@@ -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);
|
||||
|
||||
+43
-48
@@ -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: [],
|
||||
};
|
||||
}
|
||||
|
||||
+18
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
+2
-9
@@ -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();
|
||||
|
||||
|
||||
+1
-1
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user