diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 5f82c99d00..7de9d17de4 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -13,6 +13,7 @@ import { IExecutor, Executor } from '@cloudbeaver/core-executor'; import { PermissionsResource, PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root'; import { ScreenService, RouterState } from '@cloudbeaver/core-routing'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; +import { BuildVersion } from '@cloudbeaver/core-utils'; import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService'; import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute'; @@ -20,10 +21,13 @@ import type { IRouteParams } from '../AdministrationItem/IRouteParams'; import { EAdminPermission } from '../EAdminPermission'; const ADMINISTRATION_ITEMS_STATE = 'administration_items_state'; -const ADMINISTRATION_INFO = 'administration_mode'; +const ADMINISTRATION_INFO = 'administration_info'; interface IAdministrationScreenInfo { - mode: boolean; + workspaceId: string; + version: string; + serverVersion: string; + configurationMode: boolean; } @injectable() @@ -46,16 +50,11 @@ export class AdministrationScreenService { } get isConfigurationMode(): boolean { - return !!this.serverConfigResource.data?.configurationMode; + return this.serverConfigResource.configurationMode; } get publicDisabled(): boolean { - if (this.serverConfigResource.data?.configurationMode - || (this.serverConfigResource.data?.licenseRequired && !this.serverConfigResource.data?.licenseValid)) { - return true; - } - - return false; + return this.serverConfigResource.publicDisabled; } readonly ensurePermissions: IExecutor; @@ -76,7 +75,10 @@ export class AdministrationScreenService { }); this.info = { - mode: false, + workspaceId: '', + version: BuildVersion.version || '', + serverVersion: '', + configurationMode: false, }; this.itemState = new Map(); this.activationEvent = new Executor(); @@ -140,16 +142,25 @@ 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, + name: string, + defaultState?: () => T, update?: boolean, validate?: (state: T) => boolean ): T | undefined { if (!this.serverConfigResource.isLoaded()) { throw new Error('Administration screen getItemState can be used only after server configuration loaded'); } - if (this.info.mode !== this.isConfigurationMode) { + if ( + this.info.workspaceId !== this.serverConfigResource.workspaceId + || this.info.configurationMode !== this.isConfigurationMode + || this.info.serverVersion !== this.serverConfigResource.serverVersion + || this.info.version !== BuildVersion.version + ) { this.clearItemsState(); - this.info.mode = this.isConfigurationMode; + this.info.workspaceId = this.serverConfigResource.workspaceId; + this.info.configurationMode = this.isConfigurationMode; + this.info.serverVersion = this.serverConfigResource.serverVersion; + this.info.version = BuildVersion.version || ''; } if (defaultState) { diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts index ade2a43743..507314282f 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts @@ -7,7 +7,7 @@ */ import { ConnectionInfoResource, CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual, NavigatorViewSettings } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuPanel } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -17,7 +17,7 @@ import { NavNodeManagerService } from '../shared/NodesManager/NavNodeManagerServ import { NodeManagerUtils } from '../shared/NodesManager/NodeManagerUtils'; @injectable() -export class NavigationTreeContextMenuService { +export class NavigationTreeContextMenuService extends Bootstrap { static nodeContextType = 'NodeWithParent'; private static nodeViewMenuItemToken = 'nodeView'; private static menuToken = 'navTreeMenu'; @@ -27,7 +27,9 @@ export class NavigationTreeContextMenuService { private navNodeManagerService: NavNodeManagerService, private notificationService: NotificationService, private connectionInfoResource: ConnectionInfoResource - ) { } + ) { + super(); + } getMenuToken(): string { return NavigationTreeContextMenuService.menuToken; @@ -144,7 +146,7 @@ export class NavigationTreeContextMenuService { ); } - registerMenuItems() { + register(): void { this.contextMenuService.addMenuItem( this.contextMenuService.getRootMenuToken(), { @@ -179,4 +181,6 @@ export class NavigationTreeContextMenuService { this.registerNodeViewMenuItem(); } + + load(): void {} } diff --git a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index 8524ba3094..48f0276314 100644 --- a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -8,7 +8,6 @@ import { computed, observable, makeObservable } from 'mobx'; -import { AppAuthService } from '@cloudbeaver/core-authentication'; import { ConnectionInfoResource, ConnectionsManagerService, @@ -17,9 +16,10 @@ import { isConnectionProvider, IConnectionProvider, isConnectionSetter, IConnectionSetter } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { ExtensionUtils, IExtension } from '@cloudbeaver/core-extensions'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import type { ITab } from '../../shared/NavigationTabs/ITab'; import { NavigationTabsService } from '../../shared/NavigationTabs/NavigationTabsService'; @@ -45,7 +45,7 @@ interface IActiveItem { } @injectable() -export class ConnectionSchemaManagerService { +export class ConnectionSchemaManagerService extends Bootstrap { get currentConnectionId(): string | null | undefined { if (!this.activeItem?.getCurrentConnectionId) { return null; @@ -125,8 +125,9 @@ export class ConnectionSchemaManagerService { private connectionsManagerService: ConnectionsManagerService, private dbDriverResource: DBDriverResource, private notificationService: NotificationService, - private appAuthService: AppAuthService + private sessionDataResource: SessionDataResource ) { + super(); makeObservable(this, { currentObjectCatalog: computed, currentObjectSchema: computed, @@ -136,16 +137,19 @@ export class ConnectionSchemaManagerService { }); } - registerCallbacks(): void { + register(): void { + this.sessionDataResource.onDataUpdate + .addHandler(this.reset.bind(this)); + this.navigationTabsService.onTabSelect .subscribe(this.onTabSelect.bind(this)); this.navigationTabsService.onTabClose .subscribe(this.onTabClose.bind(this)); - - this.appAuthService.auth.addHandler(() => this.reset()); } + load(): void {} + /** * Trigger when user select connection in dropdown */ diff --git a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts index e66a731557..4fb1e76649 100644 --- a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts @@ -6,13 +6,14 @@ * you may not use this file except in compliance with the License. */ -import { observable, action, computed, IKeyValueMap, makeObservable } from 'mobx'; +import { observable, action, computed, makeObservable } from 'mobx'; import { Subject } from 'rxjs'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; import type { IActiveView } from '@cloudbeaver/core-view'; @@ -68,6 +69,7 @@ export class NavigationTabsService { private autoSaveService: LocalStorageSaveService, private userInfoResource: UserInfoResource, private administrationScreenService: AdministrationScreenService, + sessionDataResource: SessionDataResource, appAuthService: AppAuthService ) { makeObservable(this, { @@ -120,10 +122,10 @@ export class NavigationTabsService { } ); - appAuthService.auth + sessionDataResource.onDataUpdate .addHandler(() => this.unloadTabs()) - .addPostHandler(async state => { - if (state || appAuthService.authenticated) { + .addPostHandler(async () => { + if (appAuthService.authenticated) { await this.restoreTabs(); } }); diff --git a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts index 0773200672..b80fa2390b 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts @@ -7,7 +7,7 @@ */ import { ConnectionInfoResource, ConnectionsManagerService, EConnectionFeature } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuContext } from '@cloudbeaver/core-dialogs'; import { NavigationTreeContextMenuService } from '../../NavigationTree/NavigationTreeContextMenuService'; @@ -17,15 +17,17 @@ import { EObjectFeature } from './EObjectFeature'; import { NodeManagerUtils } from './NodeManagerUtils'; @injectable() -export class ConnectionDialogsService { +export class ConnectionDialogsService extends Bootstrap { constructor( private mainMenuService: MainMenuService, private contextMenuService: ContextMenuService, private connectionsManagerService: ConnectionsManagerService, private connectionInfoResource: ConnectionInfoResource - ) {} + ) { + super(); + } - registerMenuItems(): void { + register(): void { this.mainMenuService.registerMenuItem( EMainMenu.mainMenuConnectionsPanel, { @@ -83,4 +85,6 @@ export class ConnectionDialogsService { } ); } + + load(): void {} } diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts index f8bd258354..682d0db38c 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts @@ -44,6 +44,7 @@ export class NavNodeInfoResource extends CachedMapResource { outdated: true, loading: false, withDetails: false, + exception: null, includes: [], })); } diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index cbfaa9fa0a..558c9ee759 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ -import { AppAuthService } from '@cloudbeaver/core-authentication'; import { ConnectionAuthService, Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections'; @@ -14,7 +13,7 @@ import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { IExecutor, Executor, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { - PermissionsService, EPermission, ServerService + PermissionsService, EPermission, ServerService, SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, resourceKeyList, ResourceKey, ResourceKeyUtils @@ -101,7 +100,7 @@ export class NavNodeManagerService extends Bootstrap { private connectionAuthService: ConnectionAuthService, private notificationService: NotificationService, private serverService: ServerService, - private appAuthService: AppAuthService, + private sessionDataResource: SessionDataResource, navigationService: NavigationService ) { super(); @@ -118,7 +117,7 @@ export class NavNodeManagerService extends Bootstrap { } register(): void { - this.appAuthService.auth.addHandler(this.refreshRoot.bind(this)); + this.sessionDataResource.onDataUpdate.addHandler(this.refreshRoot.bind(this)); this.connectionInfo.onSessionUpdate.addHandler(this.connectionRefreshHandler.bind(this)); this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this)); this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler.bind(this)); diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index 9bed835acb..5681024ec9 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -53,6 +53,7 @@ export class NavTreeResource extends CachedMapResource { outdated: true, loading: false, withDetails: false, + exception: null, includes: [], })); this.onDataOutdated.addHandler(navNodeInfoResource.markOutdated.bind(navNodeInfoResource)); diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts index 3fb94aecb4..27d3896e53 100644 --- a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts +++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts @@ -6,18 +6,21 @@ * you may not use this file except in compliance with the License. */ -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { EMainMenu, MainMenuService } from '../../../TopNavBar/MainMenu/MainMenuService'; import { LogViewerService } from './LogViewerService'; @injectable() -export class LogViewerMenuService { - constructor(private mainMenuService: MainMenuService, - private logViewerService: LogViewerService) { +export class LogViewerMenuService extends Bootstrap { + constructor( + private mainMenuService: MainMenuService, + private logViewerService: LogViewerService + ) { + super(); } - registerMenuItems() { + register(): void { this.mainMenuService.registerMenuItem( EMainMenu.mainMenuToolsPanel, { @@ -34,4 +37,6 @@ export class LogViewerMenuService { toggleLogViewer() { this.logViewerService.toggle(); } + + load(): void {} } diff --git a/webapp/packages/core-app/src/useAppVersion.tsx b/webapp/packages/core-app/src/useAppVersion.tsx index 2b9d42809a..aa96bdce83 100644 --- a/webapp/packages/core-app/src/useAppVersion.tsx +++ b/webapp/packages/core-app/src/useAppVersion.tsx @@ -8,19 +8,19 @@ import { useService } from '@cloudbeaver/core-di'; import { ServerService } from '@cloudbeaver/core-root'; +import { BuildVersion } from '@cloudbeaver/core-utils'; interface IAppVersion { backendVersion: string; frontendVersion: string; } -declare const _VERSION_: string | undefined; const VERSION_REGEX = /(\d+\.\d+\.\d+)/; export function useAppVersion(short = false): IAppVersion { const serverService = useService(ServerService); let backendVersion = serverService.config.data?.version || ''; - let frontendVersion = _VERSION_ || ''; + let frontendVersion = BuildVersion.version || ''; if (short) { backendVersion = VERSION_REGEX.exec(backendVersion)?.[1] ?? backendVersion; diff --git a/webapp/packages/core-authentication/src/AppAuthService.ts b/webapp/packages/core-authentication/src/AppAuthService.ts index b39a0b2adb..a4c40b30f1 100644 --- a/webapp/packages/core-authentication/src/AppAuthService.ts +++ b/webapp/packages/core-authentication/src/AppAuthService.ts @@ -8,7 +8,7 @@ import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; -import { ServerService, SessionService } from '@cloudbeaver/core-root'; +import { ServerService } from '@cloudbeaver/core-root'; import { UserInfoResource } from './UserInfoResource'; @@ -27,12 +27,11 @@ export class AppAuthService extends Bootstrap { constructor( private serverService: ServerService, - private sessionService: SessionService, private userInfoResource: UserInfoResource, ) { super(); this.auth = new Executor(); - this.sessionService.session.onDataUpdate.addHandler(async () => { await this.authUser(); }); + this.userInfoResource.onDataUpdate.addHandler(this.authUser.bind(this)); } async isAuthNeeded(): Promise { diff --git a/webapp/packages/core-authentication/src/AuthProviderService.ts b/webapp/packages/core-authentication/src/AuthProviderService.ts index 0dd6de327f..3967ad02df 100644 --- a/webapp/packages/core-authentication/src/AuthProviderService.ts +++ b/webapp/packages/core-authentication/src/AuthProviderService.ts @@ -22,7 +22,17 @@ export class AuthProviderService { this.requestAuthProvider = new Executor(); } - async requireProvider(origin: ObjectOrigin): Promise { + async requireProvider(type: string, subType?: string): Promise + async requireProvider(origin: ObjectOrigin): Promise + async requireProvider(origin: ObjectOrigin | string, subType?: string): Promise { + if (typeof origin === 'string') { + origin = { + displayName: '', + type: origin, + subType, + }; + } + const contexts = await this.requestAuthProvider.execute(origin); const provider = contexts.getContext(AuthProviderContext); @@ -30,8 +40,7 @@ export class AuthProviderService { } async processCredentials(providerId: string, credentials: Record): Promise> { - const providers = await this.authProvidersResource.load(); - const provider = providers.find(provider => provider.id === providerId); + const provider = await this.authProvidersResource.load(providerId); if (!provider) { return credentials; diff --git a/webapp/packages/core-authentication/src/AuthProvidersResource.ts b/webapp/packages/core-authentication/src/AuthProvidersResource.ts index 7815d1e3ef..3eda613f76 100644 --- a/webapp/packages/core-authentication/src/AuthProvidersResource.ts +++ b/webapp/packages/core-authentication/src/AuthProvidersResource.ts @@ -10,38 +10,89 @@ import { injectable } from '@cloudbeaver/core-di'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import { GraphQLService, - CachedDataResource, - AuthProviderInfo + AuthProviderInfo, + CachedMapResource, + ResourceKey, + resourceKeyList, + ResourceKeyUtils } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; export type AuthProvider = AuthProviderInfo; @injectable() -export class AuthProvidersResource extends CachedDataResource { +export class AuthProvidersResource extends CachedMapResource { + static keyAll = resourceKeyList(['all']); + private loadedKeyMetadata: MetadataMap; + constructor( private graphQLService: GraphQLService, private serverConfigResource: ServerConfigResource ) { - super([]); + super(); + this.loadedKeyMetadata = new MetadataMap(() => false); this.serverConfigResource.onDataOutdated.addHandler(() => this.markOutdated()); - this.serverConfigResource.onDataUpdate.addHandler(async () => { await this.load(); }); + this.serverConfigResource.onDataUpdate.addHandler(async () => { await this.load(AuthProvidersResource.keyAll); }); + this.addAlias(AuthProvidersResource.keyAll, key => { + if (this.keys.length > 0) { + return resourceKeyList(this.keys); + } + return key; + }); } has(id: string): boolean { - return this.data.some(provider => provider.id === id); + if (this.loadedKeyMetadata.has(id)) { + return this.loadedKeyMetadata.get(id); + } + + return this.data.has(id); } - getEnabledProviders(): string[] { - return this.serverConfigResource.data?.enabledAuthProviders ?? []; + getEnabledProviders(): AuthProvider[] { + return this.get(resourceKeyList(this.serverConfigResource.enabledAuthProviders)) as AuthProvider[]; } isEnabled(id: string): boolean { - return this.serverConfigResource.data?.enabledAuthProviders.includes(id) ?? false; + return this.serverConfigResource.enabledAuthProviders.includes(id); } - protected async loader(): Promise { + async loadAll(): Promise { + this.resetIncludes(); + await this.load(AuthProvidersResource.keyAll); + + return this.values; + } + + async refreshAll(): Promise { + this.resetIncludes(); + await this.refresh(AuthProvidersResource.keyAll); + return this.values; + } + + refreshAllLazy(): void { + this.resetIncludes(); + this.markOutdated(AuthProvidersResource.keyAll); + this.loadedKeyMetadata.set(AuthProvidersResource.keyAll.list[0], false); + } + + protected async loader(key: ResourceKey): Promise> { const { providers } = await this.graphQLService.sdk.getAuthProviders(); - return providers; + + const all = ResourceKeyUtils.includes(AuthProvidersResource.keyAll, key); + + if (all) { + this.data.clear(); + } + + for (const provider of providers) { + this.data.set(provider.id, provider as AuthProvider); + } + + if (all) { + this.loadedKeyMetadata.set(AuthProvidersResource.keyAll.list[0], true); + } + return this.data; } } diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 72650de30f..331a3fd846 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -7,8 +7,8 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { SessionResource } from '@cloudbeaver/core-root'; -import { CachedDataResource, GraphQLService, UserInfo } from '@cloudbeaver/core-sdk'; +import { SessionDataResource, SessionResource } from '@cloudbeaver/core-root'; +import { CachedDataResource, GraphQLService, UserAuthToken, UserInfo } from '@cloudbeaver/core-sdk'; import { AuthProviderService } from './AuthProviderService'; @@ -17,12 +17,13 @@ export class UserInfoResource extends CachedDataResource constructor( private graphQLService: GraphQLService, private authProviderService: AuthProviderService, - private sessionResource: SessionResource + private sessionResource: SessionResource, + private sessionDataResource: SessionDataResource ) { super(null); - this.sessionResource.onDataOutdated.addHandler(() => this.markOutdated()); - this.sessionResource.onDataUpdate.addHandler(async () => { await this.load(); }); + this.sync(this.sessionResource); + this.onDataOutdated.addHandler(() => this.sessionDataResource.markOutdated()); } getId(): string { @@ -40,38 +41,42 @@ export class UserInfoResource extends CachedDataResource } async login(provider: string, credentials: Record): Promise { - const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); + await this.performUpdate(undefined, undefined, async () => { + const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); - // TODO: will be replaced with another function - const { user } = await this.graphQLService.sdk.authLogin({ - provider, - credentials: processedCredentials, + // TODO: will be replaced with another function + const { authToken } = await this.graphQLService.sdk.authLogin({ + provider, + credentials: processedCredentials, + customIncludeOriginDetails: true, + }); + + if (this.data === null) { + this.data = await this.loader(); + } else { + this.data.authTokens.push(authToken as UserAuthToken); + } }); - - await this.refresh(); - await this.updateSession(); + await this.sessionDataResource.refresh(); return this.data; } async logout(): Promise { - if (this.data) { - await this.graphQLService.sdk.authLogout(); - this.data = null; - await this.updateSession(); - } + await this.performUpdate(undefined, undefined, async () => { + if (this.data) { + await this.graphQLService.sdk.authLogout(); + this.data = null; + this.sessionDataResource.refresh(); + } + }); } protected async loader(): Promise { - await this.sessionResource.load(); const { user } = await this.graphQLService.sdk.getActiveUser({ customIncludeOriginDetails: true, }); return (user as UserInfo | null) ?? null; } - - private async updateSession() { - await this.sessionResource.refresh(); - } } diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index 30f1dbaa19..816731e2d0 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -7,6 +7,7 @@ */ import { injectable } from '@cloudbeaver/core-di'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import { GraphQLService, CachedMapResource, @@ -43,11 +44,13 @@ export class UsersResource extends CachedMapResource; constructor( private graphQLService: GraphQLService, + private serverConfigResource: ServerConfigResource, private authProviderService: AuthProviderService, private authInfoService: AuthInfoService ) { super(); this.loadedKeyMetadata = new MetadataMap(() => false); + this.serverConfigResource.onDataUpdate.addHandler(this.refreshAllLazy.bind(this)); } isNew(id: string): boolean { @@ -188,8 +191,6 @@ export class UsersResource extends CachedMapResource, 'onChange' | type ControlledProps = BaseProps & { name?: string; - value?: string; - onChange?: (value: string, name?: string) => any; + value?: string | number; + mapState?: (value: string | number) => string | number; + mapValue?: (value: string | number) => string | number; + onChange?: (value: string | number, name?: string) => any; state?: never; autoHide?: never; }; @@ -46,7 +48,9 @@ type ControlledProps = BaseProps & { type ObjectProps = BaseProps & { name: TKey; state: TState; - onChange?: (value: string, name: TKey) => any; + mapState?: (value: TState[TKey]) => TState[TKey]; + mapValue?: (value: TState[TKey]) => TState[TKey]; + onChange?: (value: TState[TKey], name: TKey) => any; autoHide?: boolean; value?: never; }; @@ -62,6 +66,8 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ value: valueControlled, required, state, + mapState, + mapValue, children, className, description, @@ -77,23 +83,29 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ const context = useContext(FormContext); const handleChange = useCallback((event: React.ChangeEvent) => { + const value = mapValue?.(event.target.value) ?? event.target.value; + if (state) { - state[name] = event.target.value; + state[name] = value; } if (onChange) { - onChange(event.target.value, name); + onChange(value, name); } if (context) { - context.onChange(event.target.value, name); + context.onChange(value, name); } }, [state, name, context, onChange]); - const value = state ? state[name] : valueControlled; - if (autoHide && !isControlPresented(name, state)) { return null; } + let value = state ? state[name] : valueControlled; + + if (mapState) { + value = mapState(value); + } + return styled(styles)( {children}{required && ' *'} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx index 807147602b..7b71ceaa4b 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx @@ -110,7 +110,7 @@ export const TreeNodeFilter: React.FC = function TreeNodeFilter({ value={context.filterValue} {...use({ filterEnabled })} onKeyDown={onKeyDown} - onChange={value => context.filter(value.trim())} + onChange={value => context.filter(String(value).trim())} /> diff --git a/webapp/packages/core-blocks/src/useDataResource.ts b/webapp/packages/core-blocks/src/useDataResource.ts index 935bf85d3e..768a1538ae 100644 --- a/webapp/packages/core-blocks/src/useDataResource.ts +++ b/webapp/packages/core-blocks/src/useDataResource.ts @@ -48,6 +48,8 @@ export function useDataResource< const key = keyObj && typeof keyObj === 'object' && 'includes' in keyObj ? keyObj.key : keyObj; const includes = keyObj && typeof keyObj === 'object' && 'includes' in keyObj ? keyObj.includes : []; + const outdated = resource.isOutdated(key); + const refObj = useObjectRef({ resource, key, @@ -100,14 +102,14 @@ export function useDataResource< return refObj.exception; }, get data() { - return resource.data; + return refObj.resource.data; }, isLoaded: () => { if (refObj.key === null) { return false; } - return resource.isLoaded(refObj.key, refObj.includes); + return refObj.resource.isLoaded(refObj.key, refObj.includes); }, reload: () => { refObj.load(); @@ -118,10 +120,10 @@ export function useDataResource< } if (refObj.key === undefined) { - return resource.isLoading(); + return refObj.resource.isLoading(); } - return resource.isDataLoading(refObj.key); + return refObj.resource.isDataLoading(refObj.key); }, })); @@ -129,7 +131,7 @@ export function useDataResource< if (exception === null) { refObj.load(); } - }, [key, includes]); + }, [key, includes, outdated]); return result; } diff --git a/webapp/packages/core-blocks/src/useMapResource.ts b/webapp/packages/core-blocks/src/useMapResource.ts index 352c01630a..2176ef407d 100644 --- a/webapp/packages/core-blocks/src/useMapResource.ts +++ b/webapp/packages/core-blocks/src/useMapResource.ts @@ -58,8 +58,13 @@ export function useMapResource< const resource = ctor instanceof CachedMapResource ? ctor : useService(ctor); const notifications = useService(NotificationService); const [exception, setException] = useState(null); - const key = keyObj && typeof keyObj === 'object' && 'includes' in keyObj ? keyObj.key : keyObj; - const includes = keyObj && typeof keyObj === 'object' && 'includes' in keyObj ? keyObj.includes : []; + let key: TKeyArg = keyObj as TKeyArg; + let includes: TIncludes = [] as TIncludes; + + if (isKeyWithIncludes(keyObj)) { + key = keyObj.key; + includes = keyObj.includes; + } const refObj = useObjectRef({ resource, @@ -77,6 +82,8 @@ export function useMapResource< actions, }); + const outdated = resource.isOutdated(key); + refObj.load = async function load() { const { resource, actions, prevData } = refObj; @@ -87,7 +94,7 @@ export function useMapResource< return; } - const newData = await resource.load(key, includes); + const newData = await resource.load(key, includes as any); try { await actions?.onData?.( @@ -124,7 +131,7 @@ export function useMapResource< return true; } - return resource.isLoaded(refObj.key, refObj.includes); + return resource.isLoaded(refObj.key, refObj.includes as any); }, reload: () => { setException(null); @@ -143,7 +150,11 @@ export function useMapResource< if (exception === null) { refObj.load(); } - }, [key, includes]); + }, [key, includes, outdated]); return result; } + +function isKeyWithIncludes(obj: any): obj is KeyWithIncludes { + return obj && typeof obj === 'object' && 'includes' in obj && 'key' in obj; +} diff --git a/webapp/packages/core-bootstrap/src/AppBootstrap.ts b/webapp/packages/core-bootstrap/src/AppBootstrap.ts deleted file mode 100644 index cf5a979cde..0000000000 --- a/webapp/packages/core-bootstrap/src/AppBootstrap.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 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 { - ConnectionDialogsService, - NavigationTreeContextMenuService, - LogViewerMenuService, - ConnectionSchemaManagerService -} from '@cloudbeaver/core-app'; -import { injectable } from '@cloudbeaver/core-di'; -import { ExceptionsCatcherService } from '@cloudbeaver/core-events'; -import { SessionExpireService } from '@cloudbeaver/core-root'; -import { ThemeService } from '@cloudbeaver/core-theming'; - -/** - * AppBootstrap.init() will be executed between first and second phase of App initialization, - * when application has registered all dependencies but plugins did'nt initialized. - * It is required to run this code in front of other initialization steps. - * - */ -@injectable() -export class AppBootstrap { - constructor( - private exceptionsCatcherService: ExceptionsCatcherService, - private themeService: ThemeService, - private connectionDialogService: ConnectionDialogsService, - private logViewerMenuService: LogViewerMenuService, - private sessionExpireService: SessionExpireService, - private navigationTreeContextMenuService: NavigationTreeContextMenuService, - private connectionSchemaManagerService: ConnectionSchemaManagerService - ) { } - - register(): void | Promise { - this.exceptionsCatcherService.subscribe(); - this.sessionExpireService.subscribe(); - this.connectionSchemaManagerService.registerCallbacks(); - this.navigationTreeContextMenuService.registerMenuItems(); - this.connectionDialogService.registerMenuItems(); - this.logViewerMenuService.registerMenuItems(); - } - - async load() { - await this.themeService.init(); - } - - async doAfterPluginsInit() { - // todo this should be moved to the NavigationTabs component creation phase but now it leads to bugs - // await this.navigationTabsService.restoreTabs(); - } -} diff --git a/webapp/packages/core-bootstrap/src/bootstrap.ts b/webapp/packages/core-bootstrap/src/bootstrap.ts index 90ef3daa67..036b3fb673 100644 --- a/webapp/packages/core-bootstrap/src/bootstrap.ts +++ b/webapp/packages/core-bootstrap/src/bootstrap.ts @@ -10,30 +10,22 @@ import { configure } from 'mobx'; import { App, PluginManifest } from '@cloudbeaver/core-di'; -import { AppBootstrap } from './AppBootstrap'; import { showErrorPage } from './ErrorPage'; import { renderLayout } from './renderLayout'; -export async function bootstrap(plugins: PluginManifest[]) { +export async function bootstrap(plugins: PluginManifest[]): Promise { configure({ enforceActions: 'never' }); + const app = new App(plugins); app.registerServices(); - const appBootstrap = app.getServiceInjector().resolveServiceByClass(AppBootstrap); - try { await app.initializeServices(); - await appBootstrap.register(); await app.loadServices(); - await app.load(); - await app.initializePlugins(); - await appBootstrap.load(); - await appBootstrap.doAfterPluginsInit(); - await app.load(); renderLayout(app.getServiceInjector()); } catch (e) { - console.log(e); + console.error(e); showErrorPage(); throw e; } diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 9895ca7b9c..56933cf17e 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -85,6 +85,7 @@ import { ServerSettingsService, ServerConfigResource, PermissionsResource, + SessionDataResource, SessionResource, SessionExpireService } from '@cloudbeaver/core-root'; @@ -127,6 +128,7 @@ export const coreManifest: PluginManifest = { ServerConfigResource, PermissionsResource, SessionResource, + SessionDataResource, SessionSettingsService, PermissionsService, CoreSettingsService, @@ -188,9 +190,4 @@ export const coreManifest: PluginManifest = { ThemeService, ServerService, ], - - async initialize() { - // Note that the initialization of the core occurs in AppBootstrap - // and it is called before the initialization phase of all manifests - }, }; diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts index 4389dc6c90..85e70d8619 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts @@ -64,7 +64,7 @@ export class ConnectionsAdministrationService extends Bootstrap { onDeActivate: async (configuration: boolean, outside: boolean) => { if (outside) { this.connectionsResource.cleanNewFlags(); - await this.connectionInfoResource.refreshSession(); + await this.connectionInfoResource.refreshUserConnections(); } }, }); diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnectionService.ts b/webapp/packages/core-connections/src/Administration/Connections/CreateConnectionService.ts index 8a5cf1770a..5589c20391 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnectionService.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnectionService.ts @@ -50,11 +50,14 @@ export class CreateConnectionService { } getDefault(): string | null { - if (this.tabsContainer.tabInfoList.length === 0) { + const tabs = this.tabsContainer.getDisplayed(); + + if (tabs.length === 0) { return null; } + if (this.administrationScreenService.isConfigurationMode) { - const sorted = this.tabsContainer.tabInfoList.sort((a, b) => { + const sorted = tabs.sort((a, b) => { const aPriority = a.options?.configurationWizard?.activationPriority || Number.MAX_SAFE_INTEGER; const bPriority = b.options?.configurationWizard?.activationPriority || Number.MAX_SAFE_INTEGER; @@ -63,7 +66,8 @@ export class CreateConnectionService { return sorted[0].key; } - return this.tabsContainer.tabInfoList[0].key; + + return tabs[0].key; } setCreateMethod(method?: string | null): void { @@ -105,7 +109,9 @@ export class CreateConnectionService { close(): void { this.clearConnectionTemplate(); - for (const method of this.tabsContainer.tabInfoList) { + const tabs = this.tabsContainer.getDisplayed(); + + for (const method of tabs) { method.options?.close?.(); } } diff --git a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts index c53e222cfb..818face237 100644 --- a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts +++ b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts @@ -11,6 +11,7 @@ import { Observable, Subject } from 'rxjs'; import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, ConnectionConfig, @@ -41,7 +42,8 @@ export class ConnectionsResource extends CachedMapResource; constructor( - private graphQLService: GraphQLService + private graphQLService: GraphQLService, + sessionDataResource: SessionDataResource ) { super(['includeOrigin', 'customIncludeNetworkHandlerCredentials', 'includeAuthProperties']); @@ -49,6 +51,8 @@ export class ConnectionsResource extends CachedMapResource this.markOutdated()); + this.changed = false; this.connectionCreateSubject = new Subject(); this.onConnectionCreate = this.connectionCreateSubject.asObservable(); diff --git a/webapp/packages/core-connections/src/ConnectionAuthService.ts b/webapp/packages/core-connections/src/ConnectionAuthService.ts index a3a0b8059d..f817f24562 100644 --- a/webapp/packages/core-connections/src/ConnectionAuthService.ts +++ b/webapp/packages/core-connections/src/ConnectionAuthService.ts @@ -44,6 +44,7 @@ export class ConnectionAuthService { return connection; } + await this.connectionInfoResource.waitLoad(); connection = this.connectionInfoResource.get(connectionId); if (!connection) { diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 8358657efd..2084a4de93 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -8,10 +8,9 @@ import { action, makeObservable } from 'mobx'; -import { AppAuthService } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor'; -import { SessionResource } from '@cloudbeaver/core-root'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, CachedMapResource, @@ -47,18 +46,21 @@ export const DEFAULT_NAVIGATOR_VIEW_SETTINGS: NavigatorSettingsInput = { export class ConnectionInfoResource extends CachedMapResource { readonly onConnectionCreate: IExecutor; readonly onConnectionClose: IExecutor; + + /** + * @deprecated Should be refactored according new SessionDataResource + */ readonly onSessionUpdate: IExecutor; private sessionUpdate: boolean; constructor( private graphQLService: GraphQLService, private connectionsResource: ConnectionsResource, - appAuthService: AppAuthService, - sessionResource: SessionResource + sessionDataResource: SessionDataResource ) { super(); makeObservable(this, { - refreshSession: action, + refreshUserConnections: action, createFromTemplate: action, createConnection: action, createFromNode: action, @@ -76,11 +78,12 @@ export class ConnectionInfoResource extends CachedMapResource this.sessionUpdate)); this.onItemDelete.addHandler(ExecutorInterrupter.interrupter(() => this.sessionUpdate)); this.onConnectionCreate.addHandler(ExecutorInterrupter.interrupter(() => this.sessionUpdate)); - sessionResource.onDataOutdated.addHandler(() => this.markOutdated()); - appAuthService.auth.addHandler(() => this.refreshSession(true)); + + sessionDataResource.onDataOutdated.addHandler(() => this.markOutdated()); + sessionDataResource.onDataUpdate.addHandler(() => this.refreshUserConnections(true)); } - async refreshSession(sessionUpdate?: boolean): Promise { + async refreshUserConnections(sessionUpdate?: boolean): Promise { this.sessionUpdate = sessionUpdate === true; try { const connectionsList = resourceKeyList(Array.from(this.data.keys())); diff --git a/webapp/packages/core-connections/src/ContainerResource.ts b/webapp/packages/core-connections/src/ContainerResource.ts index c6c8658f4a..185e36c47e 100644 --- a/webapp/packages/core-connections/src/ContainerResource.ts +++ b/webapp/packages/core-connections/src/ContainerResource.ts @@ -44,7 +44,13 @@ string private connectionInfoResource: ConnectionInfoResource ) { super(new MetadataMap(() => ({ }))); - this.metadata = new MetadataMap(() => ({ outdated: true, loading: false, outdatedData: [], loadingData: [] })); + this.metadata = new MetadataMap(() => ({ + outdated: true, + loading: false, + exception: null, + outdatedData: [], + loadingData: [], + })); this.connectionInfoResource.onItemDelete.addHandler( key => ResourceKeyUtils.forEach(key, key => this.data.delete(key)) ); @@ -92,19 +98,20 @@ string metadata.loadingData = metadata.loadingData.filter(id => id !== catalogId); } - markOutdated(param: ObjectContainerParams): void { + async markOutdated(param: ObjectContainerParams): Promise { const catalogId = param.catalogId ?? defaultCatalog; const metadata = this.metadata.get(param.connectionId); if (!metadata.outdatedData.includes(catalogId)) { metadata.outdatedData.push(catalogId); } - this.onDataOutdated.execute(param); + await this.onDataOutdated.execute(param); } markUpdated(param: ObjectContainerParams): void { const metadata = this.metadata.get(param.connectionId); const catalogId = param.catalogId ?? defaultCatalog; + metadata.exception = null; metadata.outdatedData = metadata.outdatedData.filter(id => id !== catalogId); } diff --git a/webapp/packages/core-di/src/App.ts b/webapp/packages/core-di/src/App.ts index 7433c9e133..51bb6b627d 100644 --- a/webapp/packages/core-di/src/App.ts +++ b/webapp/packages/core-di/src/App.ts @@ -44,9 +44,6 @@ export class App { // first phase register all dependencies registerServices(): void { for (const plugin of this.plugins) { - if (plugin.registerServices) { - plugin.registerServices(this.getServiceCollection()); - } if (plugin.providers?.length) { plugin.providers.forEach(provider => { // console.log('provider', provider.name); @@ -83,23 +80,4 @@ export class App { } } } - - // second phase - run init scripts todo run it based on dependency tree - async initializePlugins(): Promise { - for (const plugin of this.plugins) { - if (plugin.initialize) { - await plugin.initialize(this.getServiceInjector()); - } - } - } - - // third initialization phase? (never called) - async load(): Promise { - for (const plugin of this.plugins) { - if (plugin.load) { - // todo run it based on dependency tree - await plugin.load(); - } - } - } } diff --git a/webapp/packages/core-di/src/PluginManifest.ts b/webapp/packages/core-di/src/PluginManifest.ts index b399026023..145a158c3a 100644 --- a/webapp/packages/core-di/src/PluginManifest.ts +++ b/webapp/packages/core-di/src/PluginManifest.ts @@ -6,9 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { - IServiceConstructor, IServiceCollection, IServiceInjector -} from './IApp'; +import type { IServiceConstructor } from './IApp'; export interface PluginManifest { info: { @@ -16,24 +14,8 @@ export interface PluginManifest { defaultSettings?: Record; }; - /** - * First phase, only register services in DI here - */ - registerServices?: (services: IServiceCollection) => void; - providers: Array>; - /** - * Second phase. - * You can be sure that all services of all plugins are already registered here and you can use any service - */ - initialize?: (services: IServiceInjector) => Promise | void; - - /** - * Third phase but we still don't know what is it :) - */ - load?: () => Promise | void; - /** * The list of plugins which your plugin depends on */ diff --git a/webapp/packages/core-events/src/ExceptionsCatcherService.ts b/webapp/packages/core-events/src/ExceptionsCatcherService.ts index ce626adabe..ea0771126f 100644 --- a/webapp/packages/core-events/src/ExceptionsCatcherService.ts +++ b/webapp/packages/core-events/src/ExceptionsCatcherService.ts @@ -6,24 +6,28 @@ * you may not use this file except in compliance with the License. */ -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from './NotificationService'; @injectable() -export class ExceptionsCatcherService { +export class ExceptionsCatcherService extends Bootstrap { baseCatcher: OnErrorEventHandler | null = null; private ignored: string[] = []; private messageTitle = 'Uncatched exception'; - constructor(private notificationService: NotificationService) { } + constructor(private notificationService: NotificationService) { + super(); + } - subscribe() { + register(): void { this.baseCatcher = window.onerror; window.onerror = this.catcher; } + load(): void {} + unsubscribe() { window.onerror = this.baseCatcher; } diff --git a/webapp/packages/core-executor/src/ExecutorInterrupter.ts b/webapp/packages/core-executor/src/ExecutorInterrupter.ts index fba33286ee..704fe21d92 100644 --- a/webapp/packages/core-executor/src/ExecutorInterrupter.ts +++ b/webapp/packages/core-executor/src/ExecutorInterrupter.ts @@ -14,6 +14,12 @@ export interface IExecutorInterrupter { } export const ExecutorInterrupter = { + isInterrupted(contexts: IExecutionContextProvider): boolean { + const interrupt = contexts.getContext(ExecutorInterrupter.interruptContext); + + return interrupt.interrupted; + }, + interrupt(contexts: IExecutionContextProvider): void { const interrupt = contexts.getContext(ExecutorInterrupter.interruptContext); @@ -29,11 +35,11 @@ export const ExecutorInterrupter = { }; }, - interrupter(flag: () => boolean) { - return (data: any, contexts: IExecutionContextProvider): void => { + interrupter(flag: () => Promise | boolean) { + return async (data: any, contexts: IExecutionContextProvider): Promise => { const interrupt = contexts.getContext(ExecutorInterrupter.interruptContext); - if (flag()) { + if ((await flag())) { interrupt.interrupt(); } }; diff --git a/webapp/packages/core-executor/src/IExecutorHandler.ts b/webapp/packages/core-executor/src/IExecutorHandler.ts index 8d3dc7b8c2..a6af14a878 100644 --- a/webapp/packages/core-executor/src/IExecutorHandler.ts +++ b/webapp/packages/core-executor/src/IExecutorHandler.ts @@ -11,4 +11,4 @@ import type { IExecutionContextProvider } from './IExecutionContext'; export type IExecutorHandler = ( data: T, contexts: IExecutionContextProvider -) => void | Promise; +) => any | Promise; diff --git a/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts b/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts index 177eef0bd6..4d5cfc465c 100644 --- a/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts +++ b/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts @@ -34,7 +34,9 @@ export class TaskScheduler { async schedule( id: TIdentifier, promise: () => Promise, - after?: () => Promise | void, + after?: () => Promise | any, + success?: () => Promise | any, + error?: (exception: Error) => Promise | any, ): Promise { const task: ITask = { id, @@ -44,7 +46,12 @@ export class TaskScheduler { this.queue.push(task); try { - return await task.task; + const value = await task.task; + await success?.(); + return value; + } catch (exception) { + await error?.(exception); + throw exception; } finally { this.queue.splice(this.queue.indexOf(task), 1); await after?.(); @@ -61,7 +68,10 @@ export class TaskScheduler { } } - private async scheduler(id: TIdentifier, promise: () => Promise) { + private async scheduler( + id: TIdentifier, + promise: () => Promise, + ) { if (!this.isBlocked) { return promise(); } diff --git a/webapp/packages/core-root/src/PermissionsResource.ts b/webapp/packages/core-root/src/PermissionsResource.ts index 1da86f6602..6d8875d754 100644 --- a/webapp/packages/core-root/src/PermissionsResource.ts +++ b/webapp/packages/core-root/src/PermissionsResource.ts @@ -9,27 +9,29 @@ import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService, CachedDataResource } from '@cloudbeaver/core-sdk'; -import { SessionResource } from './SessionResource'; +import { SessionDataResource } from './SessionDataResource'; @injectable() export class PermissionsResource extends CachedDataResource, void> { constructor( private graphQLService: GraphQLService, - private sessionResource: SessionResource + sessionDataResource: SessionDataResource ) { super(new Set()); - this.sessionResource.onDataOutdated.addHandler(this.markOutdated.bind(this)); - this.sessionResource.onDataUpdate.addHandler(() => { this.load(); }); + this.sync(sessionDataResource); } has(id: string): boolean { return this.data.has(id); } - protected async loader(): Promise> { - await this.sessionResource.load(); + async hasAsync(id: string): Promise { + await this.load(); + return this.has(id); + } + protected async loader(): Promise> { const { permissions } = await this.graphQLService.sdk.sessionPermissions(); this.data.clear(); diff --git a/webapp/packages/core-root/src/PermissionsService.ts b/webapp/packages/core-root/src/PermissionsService.ts index 5a89188cc1..abae2d4d1d 100644 --- a/webapp/packages/core-root/src/PermissionsService.ts +++ b/webapp/packages/core-root/src/PermissionsService.ts @@ -26,8 +26,7 @@ export class PermissionsService { } async hasAsync(id: string): Promise { - await this.permissions.load(); - return this.has(id); + return this.permissions.hasAsync(id); } async update(): Promise { diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index 3e9015c68b..92fc289f26 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -6,24 +6,91 @@ * you may not use this file except in compliance with the License. */ +import { makeObservable, observable } from 'mobx'; + import { injectable } from '@cloudbeaver/core-di'; -import { GraphQLService, CachedDataResource, ServerConfig } from '@cloudbeaver/core-sdk'; +import { GraphQLService, CachedDataResource, ServerConfig, ServerConfigInput } from '@cloudbeaver/core-sdk'; @injectable() export class ServerConfigResource extends CachedDataResource { + update: ServerConfigInput; + constructor( private graphQLService: GraphQLService ) { super(null); + makeObservable(this, { + update: observable, + }); + + this.update = {}; } - async update(): Promise { - await this.refresh(); + get serverVersion(): string { + return this.data?.version || ''; + } + + get workspaceId(): string { + return this.data?.workspaceId || ''; + } + + get configurationMode(): boolean { + return !!this.data?.configurationMode; + } + + get publicDisabled(): boolean { + if (this.data?.configurationMode + || (this.data?.licenseRequired && !this.data?.licenseValid)) { + return true; + } + + return false; + } + + get enabledAuthProviders(): string[] { + return this.update.enabledAuthProviders || this.data?.enabledAuthProviders || []; + } + + get credentialsSaveEnabled(): boolean { + return this.update.adminCredentialsSaveEnabled ?? this.data?.adminCredentialsSaveEnabled ?? false; + } + + get userCredentialsSaveEnabled(): boolean { + return this.update.publicCredentialsSaveEnabled ?? this.data?.publicCredentialsSaveEnabled ?? false; + } + + setDataUpdate(update: ServerConfigInput): void { + this.update = update; + } + + async save(): Promise { + await this.performUpdate(undefined, undefined, async () => { + await this.graphQLService.sdk.configureServer({ + configuration: this.update, + }); + + this.data = await this.loader(); + }); } protected async loader(): Promise { const { serverConfig } = await this.graphQLService.sdk.serverConfig(); + this.update.serverName = serverConfig.name; + this.update.sessionExpireTime = serverConfig.sessionExpireTime; + + this.update.adminName = undefined; + this.update.adminPassword = undefined; + + this.update.anonymousAccessEnabled = serverConfig.anonymousAccessEnabled; + this.update.authenticationEnabled = serverConfig.authenticationEnabled; + + this.update.adminCredentialsSaveEnabled = serverConfig.adminCredentialsSaveEnabled; + this.update.publicCredentialsSaveEnabled = serverConfig.publicCredentialsSaveEnabled; + + this.update.customConnectionsEnabled = serverConfig.supportsCustomConnections; + this.update.enabledAuthProviders = serverConfig.enabledAuthProviders; + return serverConfig as ServerConfig; } } diff --git a/webapp/packages/core-root/src/SessionDataResource.ts b/webapp/packages/core-root/src/SessionDataResource.ts new file mode 100644 index 0000000000..2c43f7bacd --- /dev/null +++ b/webapp/packages/core-root/src/SessionDataResource.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { injectable } from '@cloudbeaver/core-di'; +import { CachedDataResource } from '@cloudbeaver/core-sdk'; + +import { SessionResource } from './SessionResource'; + +@injectable() +export class SessionDataResource extends CachedDataResource { + constructor( + sessionResource: SessionResource + ) { + super(null); + + this.sync(sessionResource); + } + + protected async loader(): Promise { + return null; + } +} diff --git a/webapp/packages/core-root/src/SessionError.ts b/webapp/packages/core-root/src/SessionError.ts index 08303c1288..ba38f84209 100644 --- a/webapp/packages/core-root/src/SessionError.ts +++ b/webapp/packages/core-root/src/SessionError.ts @@ -1,3 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + export class SessionError extends Error { constructor(message: string) { super(message); diff --git a/webapp/packages/core-root/src/SessionExpireService.ts b/webapp/packages/core-root/src/SessionExpireService.ts index 29d88aa9d9..f74fc3f2dc 100644 --- a/webapp/packages/core-root/src/SessionExpireService.ts +++ b/webapp/packages/core-root/src/SessionExpireService.ts @@ -8,7 +8,7 @@ import { Subject } from 'rxjs'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { GQLError, GraphQLService, EServerErrorCode } from '@cloudbeaver/core-sdk'; @@ -16,19 +16,22 @@ import { import { SessionError } from './SessionError'; @injectable() -export class SessionExpireService { +export class SessionExpireService extends Bootstrap { private isNotifiedAboutExpiredSession = false; onSessionExpire = new Subject(); constructor( private graphQLService: GraphQLService ) { + super(); } - subscribe(): void { + register(): void { this.graphQLService.registerInterceptor(this.sessionExpiredInterceptor.bind(this)); } + load(): void {} + private async sessionExpiredInterceptor(request: Promise): Promise { try { return await request; diff --git a/webapp/packages/core-root/src/SessionResource.ts b/webapp/packages/core-root/src/SessionResource.ts index 9dd964553b..bd97138e7e 100644 --- a/webapp/packages/core-root/src/SessionResource.ts +++ b/webapp/packages/core-root/src/SessionResource.ts @@ -25,17 +25,10 @@ export class SessionResource extends CachedDataResource { this.load(); }); - } - - async update(): Promise { - await this.refresh(); + this.sync(this.serverConfiguration); } protected async loader(): Promise { - await this.serverConfiguration.load(); - const { session } = await this.graphQLService.sdk.openSession(); return session; diff --git a/webapp/packages/core-root/src/SessionSettingsService.ts b/webapp/packages/core-root/src/SessionSettingsService.ts index fc2c3f06df..9776fe48f0 100644 --- a/webapp/packages/core-root/src/SessionSettingsService.ts +++ b/webapp/packages/core-root/src/SessionSettingsService.ts @@ -19,7 +19,7 @@ export class SessionSettingsService extends SettingsSource { } } - setValue(key: string, value: string) { + setValue(key: string, value: string): void { super.setValue(key, value); localStorage.setItem(this.localStorageKey, JSON.stringify(this.store)); } diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index 3948e481d7..47de13727c 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -4,7 +4,8 @@ export * from './ServerConfigResource'; export * from './SessionService'; export * from './SessionResource'; export * from './ServerService'; -export * from './usePermission'; export * from './ServerSettingsService'; +export * from './SessionDataResource'; export * from './SessionSettingsService'; export * from './SessionExpireService'; +export * from './usePermission'; diff --git a/webapp/packages/core-root/src/usePermission.ts b/webapp/packages/core-root/src/usePermission.ts index 72ca28f07a..dd6a2376a5 100644 --- a/webapp/packages/core-root/src/usePermission.ts +++ b/webapp/packages/core-root/src/usePermission.ts @@ -10,7 +10,7 @@ import { useService } from '@cloudbeaver/core-di'; import { PermissionsService } from './PermissionsService'; -export function usePermission(key: string) { +export function usePermission(key: string): boolean { const permissionsService = useService(PermissionsService); return permissionsService.has(key); diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index fa33e01f91..ae307c3ffc 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -72,6 +72,7 @@ export abstract class CachedMapResource< this.metadata = new MetadataMap(() => ({ outdated: true, loading: false, + exception: null, includes: [...this.defaultIncludes], })); @@ -82,6 +83,7 @@ export abstract class CachedMapResource< } isIncludes(key: ResourceKey, includes: CachedResourceIncludeArgs): boolean { + key = this.transformParam(key); return ResourceKeyUtils.every(key, key => { const metadata = this.metadata.get(key); @@ -90,14 +92,17 @@ export abstract class CachedMapResource< } isOutdated(key: ResourceKey): boolean { + key = this.transformParam(key); return ResourceKeyUtils.some(key, key => this.metadata.get(key).outdated); } isDataLoading(key: ResourceKey): boolean { + key = this.transformParam(key); return ResourceKeyUtils.some(key, key => this.metadata.get(key).loading); } markDataLoading(key: ResourceKey, includes?: string[]): void { + key = this.transformParam(key); ResourceKeyUtils.forEach(key, key => { const metadata = this.metadata.get(key); metadata.loading = true; @@ -105,6 +110,8 @@ export abstract class CachedMapResource< } markDataLoaded(key: ResourceKey, includes?: string[]): void { + key = this.transformParam(key); + if (includes) { this.commitIncludes(key, includes); } @@ -115,18 +122,32 @@ export abstract class CachedMapResource< }); } - markOutdated(): void - markOutdated(key: ResourceKey): void - markOutdated(key?: ResourceKey): void { + async markDataError(exception: Error, key: ResourceKey): Promise { + key = this.transformParam(key); + + ResourceKeyUtils.forEach(key, key => { + const metadata = this.metadata.get(key); + metadata.exception = exception; + }); + + await this.onDataError.execute({ param: key, exception }); + } + + markOutdated(): Promise + markOutdated(key: ResourceKey): Promise + async markOutdated(key?: ResourceKey): Promise { if (!key) { key = resourceKeyList(Array.from(this.data.keys())); + } else { + key = this.transformParam(key); } ResourceKeyUtils.forEach(key, key => { const metadata = this.metadata.get(key); metadata.outdated = true; }); - this.onDataOutdated.execute(key); + + await this.onDataOutdated.execute(key); } markUpdated(): void @@ -134,15 +155,19 @@ export abstract class CachedMapResource< markUpdated(key?: ResourceKey): void { if (!key) { key = resourceKeyList(Array.from(this.data.keys())); + } else { + key = this.transformParam(key); } ResourceKeyUtils.forEach(key, key => { const metadata = this.metadata.get(key); metadata.outdated = false; + metadata.exception = null; }); } isLoaded(key: ResourceKey, includes?: CachedResourceIncludeArgs): boolean { + key = this.transformParam(key); return ResourceKeyUtils.every(key, key => { if (!this.has(key)) { return false; @@ -163,12 +188,14 @@ export abstract class CachedMapResource< get(key: ResourceKeyList): Array; get(key: ResourceKey): Array| TValue | undefined; get(key: ResourceKey): Array| TValue | undefined { + key = this.transformParam(key); return ResourceKeyUtils.map(key, key => this.data.get(key)); } set(key: TKey, value: TValue): void; set(key: ResourceKeyList, value: TValue[]): void; set(key: ResourceKey, value: TValue | TValue[]): void { + key = this.transformParam(key); ResourceKeyUtils.forEach(key, (key, i) => { if (i === -1) { this.data.set(key, value as TValue); @@ -184,6 +211,7 @@ export abstract class CachedMapResource< delete(key: ResourceKeyList): void; delete(key: ResourceKey): void; delete(key: ResourceKey): void { + key = this.transformParam(key); ResourceKeyUtils.forEach(key, key => this.data.delete(key)); this.markUpdated(key); this.onItemDelete.execute(key); @@ -230,6 +258,7 @@ export abstract class CachedMapResource< } has(key: TKey): boolean { + key = this.transformParam(key) as TKey; return this.data.has(key); } @@ -241,6 +270,7 @@ export abstract class CachedMapResource< if (!key) { return this.defaultIncludes; } + key = this.transformParam(key); const metadata = this.metadata.get(ResourceKeyUtils.first(key)); @@ -266,6 +296,7 @@ export abstract class CachedMapResource< } protected commitIncludes(key: ResourceKey, includes: string[]): void { + key = this.transformParam(key); ResourceKeyUtils.forEach(key, key => { const metadata = this.metadata.get(key); diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index 41c2abd6e8..0e0a976488 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -8,12 +8,23 @@ import { observable, makeObservable } from 'mobx'; -import { Executor, IExecutor, TaskScheduler } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, IExecutor, TaskScheduler } from '@cloudbeaver/core-executor'; import { MetadataMap } from '@cloudbeaver/core-utils'; export interface ICachedResourceMetadata { outdated: boolean; loading: boolean; + exception: Error | null; +} + +export interface IDataError { + param: TParam; + exception: Error; +} + +export interface IParamAlias { + param: TParam; + getAlias: (param: TParam) => TParam; } export type CachedResourceData< @@ -37,13 +48,16 @@ export abstract class CachedResource< data: TData; readonly onDataOutdated: IExecutor; - readonly onDataUpdate: IExecutor; + readonly onDataUpdate: IExecutor; + readonly onDataError: IExecutor>; + readonly beforeLoad: IExecutor; protected metadata: MetadataMap; protected loading = false; protected scheduler: TaskScheduler; + protected paramAliases: Array>; constructor(defaultValue: TData) { makeObservable, 'loading'>(this, { @@ -54,11 +68,23 @@ export abstract class CachedResource< this.includes = this.includes.bind(this); this.loadingTask = this.loadingTask.bind(this); - this.metadata = new MetadataMap(() => ({ outdated: true, loading: false })); + this.paramAliases = []; + this.metadata = new MetadataMap(() => ({ outdated: true, loading: false, exception: null })); this.scheduler = new TaskScheduler(this.includes); this.data = defaultValue; + this.beforeLoad = new Executor(null, this.includes); this.onDataOutdated = new Executor(null, this.includes); - this.onDataUpdate = new Executor(); + this.onDataUpdate = new Executor(null, this.includes); + this.onDataError = new Executor>(null, (a, b) => this.includes(a.param, b.param)); + } + + sync( + resource: CachedResource, + context: TContext + ): void { + resource.onDataOutdated.addHandler(param => this.markOutdated(param)); + resource.onDataUpdate.addHandler(param => this.load(param, context)); + this.beforeLoad.addHandler(param => resource.load(param)); } abstract isLoaded(param: TParam, context: TContext): boolean; @@ -72,45 +98,87 @@ export abstract class CachedResource< } isOutdated(param: TParam): boolean { + param = this.transformParam(param); return this.metadata.get(param as unknown as TKey).outdated; } isDataLoading(param: TParam): boolean { + param = this.transformParam(param); return this.metadata.get(param as unknown as TKey).loading; } markDataLoading(param: TParam, context: TContext): void { + param = this.transformParam(param); const metadata = this.metadata.get(param as unknown as TKey); metadata.loading = true; } markDataLoaded(param: TParam, context: TContext): void { + param = this.transformParam(param); const metadata = this.metadata.get(param as unknown as TKey); metadata.loading = false; } - markOutdated(param: TParam): void { + async markDataError(exception: Error, param: TParam, context: TContext): Promise { + param = this.transformParam(param); + const metadata = this.metadata.get(param as unknown as TKey); + metadata.exception = exception; + await this.onDataError.execute({ param, exception }); + } + + async markOutdated(param: TParam): Promise { + param = this.transformParam(param); const metadata = this.metadata.get(param as unknown as TKey); metadata.outdated = true; - this.onDataOutdated.execute(param); + await this.onDataOutdated.execute(param); } markUpdated(param: TParam): void { + param = this.transformParam(param); const metadata = this.metadata.get(param as unknown as TKey); metadata.outdated = false; + metadata.exception = null; + } + + addAlias(param: TParam, getAlias: (param: TParam) => TParam): void { + this.paramAliases.push({ param, getAlias }); + } + + transformParam(param: TParam): TParam { + let deep = 0; + // eslint-disable-next-line no-labels + transform: + + if (deep < 10) { + for (const alias of this.paramAliases) { + if (this.includes(alias.param, param)) { + param = alias.getAlias(param); + deep++; + // eslint-disable-next-line no-labels + break transform; + } + } + } else { + console.warn('CachedResource: parameter transform was stopped'); + } + return param; } async refresh(param: TParam, context: TContext): Promise { + param = this.transformParam(param); await this.loadData(param, true, context); return this.data; } async load(param: TParam, context: TContext): Promise { + param = this.transformParam(param); await this.loadData(param, false, context); return this.data; } protected includes(param: TParam, second: TParam): boolean { + param = this.transformParam(param); + second = this.transformParam(second); return param === second; } @@ -138,46 +206,61 @@ export abstract class CachedResource< return; } + param = this.transformParam(param); this.markDataLoading(param, context); this.loading = true; - return this.scheduler.schedule(param, async () => { + return this.scheduler.schedule( + param, + async () => { // repeated because previous task maybe has been load requested data - if (exitCheck?.()) { - return; - } + if (exitCheck?.()) { + return; + } - return await this.taskWrapper(param, context, update); - }, () => { - this.markDataLoaded(param, context); - this.loading = false; - }); + return await this.taskWrapper(param, context, update); + }, + () => { + this.markDataLoaded(param, context); + this.loading = false; + }, + () => this.onDataUpdate.execute(param), + exception => this.markDataError(exception, param, context)); } protected async loadData(param: TParam, refresh: boolean, context: TContext): Promise { + param = this.transformParam(param); + const contexts = await this.beforeLoad.execute(param); + + if (ExecutorInterrupter.isInterrupted(contexts) && !refresh) { + return; + } + if (this.isLoaded(param, context) && !this.isOutdated(param) && !refresh) { return; } this.markDataLoading(param, context); this.loading = true; - await this.scheduler.schedule(param, async () => { + await this.scheduler.schedule( + param, + async () => { // repeated because previous task maybe has been load requested data - if (this.isLoaded(param, context) && !this.isOutdated(param) && !refresh) { - return; - } + if (this.isLoaded(param, context) && !this.isOutdated(param) && !refresh) { + return; + } - await this.taskWrapper(param, context, this.loadingTask); - }, () => { - this.markDataLoaded(param, context); - this.loading = false; - }); + await this.taskWrapper(param, context, this.loadingTask); + }, + () => { + this.markDataLoaded(param, context); + this.loading = false; + }, + () => this.onDataUpdate.execute(param), + exception => this.markDataError(exception, param, context)); } private async loadingTask(param: TParam, context: TContext) { this.data = await this.loader(param, context); - - // TODO: seems should be moved to scheduler `after` callback - this.onDataUpdate.execute(this.data); } private async taskWrapper( @@ -185,7 +268,7 @@ export abstract class CachedResource< context: TContext, promise: (param: TParam, context: TContext) => Promise ) { - this.markOutdated(param); + await this.markOutdated(param); const value = await promise(param, context); this.markUpdated(param); return value; diff --git a/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql b/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql index 58d62196da..d4c30a474a 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql @@ -1,12 +1,9 @@ query authLogin( $provider: ID! $credentials: Object! + $customIncludeOriginDetails: Boolean! ) { - user: authLogin(provider: $provider, credentials: $credentials) { - userId - displayName - authProvider - loginTime - message + authToken: authLogin(provider: $provider, credentials: $credentials) { + ...AuthToken } } \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql b/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql index 6f4159f6c4..9a7c1e3167 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql @@ -3,12 +3,7 @@ query getActiveUser($customIncludeOriginDetails: Boolean!) { userId displayName authTokens { - authProvider - loginTime - message - origin { - ...ObjectOriginInfo - } + ...AuthToken } } } \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/fragments/AuthToken.gql b/webapp/packages/core-sdk/src/queries/fragments/AuthToken.gql new file mode 100644 index 0000000000..589a0d6506 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/fragments/AuthToken.gql @@ -0,0 +1,8 @@ +fragment AuthToken on UserAuthToken { + authProvider + loginTime + message + origin { + ...ObjectOriginInfo + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql index e7b269ee0b..048783dab7 100644 --- a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql +++ b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql @@ -2,6 +2,7 @@ query serverConfig { serverConfig { name version + workspaceId productConfiguration supportsCustomConnections supportsConnectionBrowser diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 105956e2e1..4cfa4ffe0d 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -19,7 +19,7 @@ export interface Scalars { export interface Query { activeUser?: Maybe; allConnections: ConnectionInfo[]; - authLogin: UserAuthInfo; + authLogin: UserAuthToken; authLogout?: Maybe; authModels: DatabaseAuthModel[]; authProviders: AuthProviderInfo[]; @@ -57,8 +57,6 @@ export interface Query { serverConfig: ServerConfig; sessionPermissions: Array>; sessionState: SessionInfo; - /** @deprecated Field no longer supported */ - sessionUser?: Maybe; setConnectionSubjectAccess?: Maybe; setDefaultNavigatorSettings: Scalars['Boolean']; setSubjectConnectionAccess?: Maybe; @@ -479,6 +477,7 @@ export interface ProductInfo { export interface ServerConfig { name: Scalars['String']; version: Scalars['String']; + workspaceId: Scalars['ID']; anonymousAccessEnabled?: Maybe; authenticationEnabled?: Maybe; supportsCustomConnections?: Maybe; @@ -926,22 +925,9 @@ export interface AuthProviderInfo { export interface UserAuthToken { authProvider: Scalars['ID']; loginTime: Scalars['DateTime']; - message?: Maybe; - origin: ObjectOrigin; -} - -export interface UserAuthInfo { - /** @deprecated Field no longer supported */ userId: Scalars['String']; - /** @deprecated Field no longer supported */ - displayName?: Maybe; - /** @deprecated Field no longer supported */ - authProvider: Scalars['String']; - /** @deprecated Field no longer supported */ - loginTime: Scalars['DateTime']; - /** @deprecated Field no longer supported */ + displayName: Scalars['String']; message?: Maybe; - /** @deprecated Field no longer supported */ origin: ObjectOrigin; } @@ -981,9 +967,10 @@ export interface AsyncTaskCancelMutation { result: Mutation['asyncTaskCancel'] } export type AuthLoginQueryVariables = Exact<{ provider: Scalars['ID']; credentials: Scalars['Object']; + customIncludeOriginDetails: Scalars['Boolean']; }>; -export interface AuthLoginQuery { user: Pick } +export interface AuthLoginQuery { authToken: AuthTokenFragment } export type AuthLogoutQueryVariables = Exact<{ [key: string]: never }>; @@ -996,10 +983,7 @@ export type GetActiveUserQueryVariables = Exact<{ export interface GetActiveUserQuery { user?: Maybe<( Pick - & { authTokens: Array<( - Pick - & { origin: ObjectOriginInfoFragment } - )>; } + & { authTokens: AuthTokenFragment[] } )>; } @@ -1341,6 +1325,11 @@ export type AdminUserInfoFragment = ( export type AllNavigatorSettingsFragment = Pick; +export type AuthTokenFragment = ( + Pick + & { origin: ObjectOriginInfoFragment } +); + export type DatabaseConnectionFragment = ( Pick & { origin: ObjectOriginInfoFragment; authProperties: UserConnectionAuthPropertiesFragment[]; networkHandlersConfig: Array, 'userName' | 'password' | 'properties'>>; navigatorSettings: AllNavigatorSettingsFragment } @@ -1576,7 +1565,7 @@ export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>; export interface ServerConfigQuery { serverConfig: ( - Pick + Pick & { supportedLanguages: Array>; defaultNavigatorSettings: AllNavigatorSettingsFragment; productInfo: Pick } ); } @@ -1654,6 +1643,16 @@ export const AdminUserInfoFragmentDoc = ` } } ${ObjectOriginInfoFragmentDoc}`; +export const AuthTokenFragmentDoc = ` + fragment AuthToken on UserAuthToken { + authProvider + loginTime + message + origin { + ...ObjectOriginInfo + } +} + ${ObjectOriginInfoFragmentDoc}`; export const UserConnectionAuthPropertiesFragmentDoc = ` fragment UserConnectionAuthProperties on ObjectPropertyInfo { id @@ -1820,16 +1819,12 @@ export const AsyncTaskCancelDocument = ` } `; export const AuthLoginDocument = ` - query authLogin($provider: ID!, $credentials: Object!) { - user: authLogin(provider: $provider, credentials: $credentials) { - userId - displayName - authProvider - loginTime - message + query authLogin($provider: ID!, $credentials: Object!, $customIncludeOriginDetails: Boolean!) { + authToken: authLogin(provider: $provider, credentials: $credentials) { + ...AuthToken } } - `; + ${AuthTokenFragmentDoc}`; export const AuthLogoutDocument = ` query authLogout { authLogout @@ -1841,16 +1836,11 @@ export const GetActiveUserDocument = ` userId displayName authTokens { - authProvider - loginTime - message - origin { - ...ObjectOriginInfo - } + ...AuthToken } } } - ${ObjectOriginInfoFragmentDoc}`; + ${AuthTokenFragmentDoc}`; export const GetAuthProvidersDocument = ` query getAuthProviders { providers: authProviders { @@ -2495,6 +2485,7 @@ export const ServerConfigDocument = ` serverConfig { name version + workspaceId productConfiguration supportsCustomConnections supportsConnectionBrowser diff --git a/webapp/packages/core-theming/src/ThemeService.ts b/webapp/packages/core-theming/src/ThemeService.ts index cfbd049e37..1b0681244c 100644 --- a/webapp/packages/core-theming/src/ThemeService.ts +++ b/webapp/packages/core-theming/src/ThemeService.ts @@ -13,7 +13,7 @@ import './styles/main/app-loading-screen.css'; import './styles/main/elevation.scss'; import './styles/main/typography.scss'; import './styles/main/color.scss'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { DbeaverError, NotificationService } from '@cloudbeaver/core-events'; import { SettingsService } from '@cloudbeaver/core-settings'; @@ -33,7 +33,7 @@ const THEME_SETTINGS_KEY = 'themeSettings'; const DEFAULT_THEME_ID = 'light'; @injectable() -export class ThemeService { +export class ThemeService extends Bootstrap { get themes(): ITheme[] { return Array.from(this.themeMap.values()); } @@ -60,6 +60,8 @@ export class ThemeService { private notificationService: NotificationService, private settingsService: SettingsService ) { + super(); + makeObservable(this, { themes: computed, currentTheme: computed, @@ -67,11 +69,13 @@ export class ThemeService { settings: observable, setCurrentThemeId: action, }); + } + register(): void { this.loadAllThemes(); } - async init() { + async load(): Promise { this.settingsService.registerSettings(this.settings, THEME_SETTINGS_KEY); await this.changeThemeAsync(this.currentThemeId); } diff --git a/webapp/packages/core-ui/package.json b/webapp/packages/core-ui/package.json index 663209eacc..3298d98f50 100644 --- a/webapp/packages/core-ui/package.json +++ b/webapp/packages/core-ui/package.json @@ -15,9 +15,13 @@ "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix" }, "peerDependencies": { + "@cloudbeaver/core-authentication": "^0.1.0", "@cloudbeaver/core-di": "^0.1.0", "@cloudbeaver/core-executor": "^0.1.0", + "@cloudbeaver/core-blocks": "^0.1.0", + "@cloudbeaver/core-sdk": "^0.1.0", "@cloudbeaver/core-dialogs": "^0.1.0", - "mobx": "^5.x.x" + "mobx": "^5.x.x", + "mobx-react-lite": "^3.x.x" } } \ No newline at end of file diff --git a/webapp/packages/core-ui/src/AuthenticationProvider.tsx b/webapp/packages/core-ui/src/AuthenticationProvider.tsx new file mode 100644 index 0000000000..78781c48cd --- /dev/null +++ b/webapp/packages/core-ui/src/AuthenticationProvider.tsx @@ -0,0 +1,78 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import styled, { css } from 'reshadow'; + +import { AuthProviderService, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { BASE_CONTAINERS_STYLES, Button, Container, GroupItem } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; + +const styles = css` + Container { + justify-content: center; + align-items: center; + } +`; + +export type Props = { + children?: () => React.ReactNode; + className?: string; +} & ({ + origin: ObjectOrigin; +} | { + type: string; + subType?: string; +}); + +export const AuthenticationProvider: React.FC = observer(function AuthenticationProvider(props) { + const translate = useTranslate(); + const style = useStyles(styles, BASE_CONTAINERS_STYLES); + const authProviderService = useService(AuthProviderService); + const userInfoService = useService(UserInfoResource); + let type: string; + let subType: string | undefined; + + if ('origin' in props) { + type = props.origin.type; + subType = props.origin.subType; + } else { + type = props.type; + subType = props.subType; + } + + const authorized = userInfoService.hasToken(type, subType); + + if (authorized) { + return props.children?.() as null || null; + } + + function handleAuth() { + authProviderService.requireProvider(type, subType); + } + + return styled(style)( + + + {translate('authentication_request_token')} + + + + + + ); +}); diff --git a/webapp/packages/core-ui/src/index.ts b/webapp/packages/core-ui/src/index.ts index 7c725d6bed..11001f83a2 100644 --- a/webapp/packages/core-ui/src/index.ts +++ b/webapp/packages/core-ui/src/index.ts @@ -1,2 +1,3 @@ export * from './Screens/AppScreen/NavigationService'; export * from './Screens/AppScreen/OptionsPanelService'; +export * from './AuthenticationProvider'; diff --git a/webapp/packages/core-utils/src/BuildVersion.ts b/webapp/packages/core-utils/src/BuildVersion.ts new file mode 100644 index 0000000000..2322d7c756 --- /dev/null +++ b/webapp/packages/core-utils/src/BuildVersion.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +declare const _VERSION_: string | undefined; + +export const BuildVersion = { + get version(): string | undefined { + return _VERSION_; + }, +}; diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index a7307525ed..41bc0758dc 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -1,3 +1,4 @@ +export * from './BuildVersion'; export * from './flat'; export * from './MetadataMap'; export * from './OrderedMap'; diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx index 20c76aed6e..af943e3123 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx @@ -12,15 +12,16 @@ import styled from 'reshadow'; import { Group, GroupTitle, BASE_CONTAINERS_STYLES, InputFieldNew } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; -import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; +import type { IServerConfigurationPageState } from '../IServerConfigurationPageState'; + interface Props { - serverConfig: ServerConfigInput; + state: IServerConfigurationPageState; } export const ServerConfigurationInfoForm: React.FC = observer(function ServerConfigurationInfoForm({ - serverConfig, + state, }) { const translate = useTranslate(); return styled(useStyles(BASE_CONTAINERS_STYLES))( @@ -29,7 +30,7 @@ export const ServerConfigurationInfoForm: React.FC = observer(function Se = observer(function Se (v ?? 1800000) / 1000 / 60} + mapValue={v => (v ?? 30) * 1000 * 60} required tiny > diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx index 7a9efaee32..ed17437595 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx @@ -24,10 +24,6 @@ import { ServerConfigurationPageController } from './ServerConfigurationPageCont import { ServerConfigurationService } from './ServerConfigurationService'; const styles = css` - Loader { - height: 400px; - } - SubmittingForm { flex: 1; display: flex; @@ -35,10 +31,6 @@ const styles = css` flex-direction: column; } - FormBox { - flex: 0; - } - p { white-space: pre-wrap; line-height: 2; @@ -75,28 +67,25 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser )} - - - {() => styled(style)( - <> - - - {translate('administration_configuration_wizard_configuration_plugins')} - - - - - - - - )} - - + + {() => styled(style)( + + + + {translate('administration_configuration_wizard_configuration_plugins')} + + + + + + + + )} + ); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 31e1763c88..305986484b 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -9,7 +9,6 @@ import { observable, makeObservable } from 'mobx'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { UsersResource } from '@cloudbeaver/core-authentication'; import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; @@ -20,8 +19,9 @@ import { GraphQLService } from '@cloudbeaver/core-sdk'; import type { IServerConfigurationPageState } from './IServerConfigurationPageState'; -export interface IConfigurationPlaceholderProps extends IServerConfigurationPageState { +export interface IConfigurationPlaceholderProps { configurationWizard: boolean; + state: IServerConfigurationPageState; } export interface IServerConfigSaveData { @@ -54,7 +54,6 @@ export class ServerConfigurationService { private readonly serverConfigResource: ServerConfigResource, private readonly graphQLService: GraphQLService, private readonly notificationService: NotificationService, - private readonly usersResource: UsersResource, ) { makeObservable(this, { state: observable, @@ -83,8 +82,6 @@ export class ServerConfigurationService { .before(this.prepareConfigTask) .addPostHandler(this.save); - this.prepareConfigTask.addHandler(this.prepareConfig); - this.validationTask .addHandler(this.validateForm) .addPostHandler(this.ensureValidation); @@ -95,18 +92,17 @@ export class ServerConfigurationService { } async loadConfig(): Promise { - let reload = false; - try { - const config = await this.serverConfigResource.load(); + let reload = false; this.state = this.administrationScreenService.getItemState( 'server-configuration', () => { reload = true; - return serverConfigStateContext(); + return this.state; }, - !config?.configurationMode + !this.administrationScreenService.isConfigurationMode ); + this.serverConfigResource.setDataUpdate(this.state.serverConfig); await this.loadConfigTask.execute({ state: this.state, @@ -145,25 +141,14 @@ export class ServerConfigurationService { return; } - if (config.configurationMode) { - data.state.serverConfig.serverName = 'CloudBeaver'; - data.state.serverConfig.sessionExpireTime = 30; + data.state.serverConfig.serverName = config.name || config.productInfo.name; + data.state.serverConfig.sessionExpireTime = config.sessionExpireTime; - data.state.serverConfig.adminCredentialsSaveEnabled = true; - data.state.serverConfig.publicCredentialsSaveEnabled = true; - data.state.serverConfig.customConnectionsEnabled = true; + data.state.serverConfig.adminCredentialsSaveEnabled = config.adminCredentialsSaveEnabled; + data.state.serverConfig.publicCredentialsSaveEnabled = config.publicCredentialsSaveEnabled; + data.state.serverConfig.customConnectionsEnabled = config.supportsCustomConnections; - data.state.navigatorConfig = { ...DEFAULT_NAVIGATOR_VIEW_SETTINGS }; - } else { - data.state.serverConfig.serverName = config.name; - data.state.serverConfig.sessionExpireTime = (config.sessionExpireTime ?? 1800000) / 1000 / 60; - - data.state.serverConfig.adminCredentialsSaveEnabled = config.adminCredentialsSaveEnabled; - data.state.serverConfig.publicCredentialsSaveEnabled = config.publicCredentialsSaveEnabled; - data.state.serverConfig.customConnectionsEnabled = config.supportsCustomConnections; - - data.state.navigatorConfig = { ...config.defaultNavigatorSettings }; - } + data.state.navigatorConfig = { ...config.defaultNavigatorSettings }; } catch (exception) { ExecutorInterrupter.interrupt(contexts); this.notificationService.logException(exception, 'Can\'t load server configuration'); @@ -178,35 +163,17 @@ export class ServerConfigurationService { }; } - private prepareConfig: IExecutorHandler = (data, contexts) => { - const state = contexts.getContext(serverConfigStateContext); - - state.serverConfig.serverName = data.state.serverConfig.serverName; - state.serverConfig.sessionExpireTime = (data.state.serverConfig.sessionExpireTime ?? 30) * 1000 * 60; - - state.serverConfig.adminCredentialsSaveEnabled = data.state.serverConfig.adminCredentialsSaveEnabled; - state.serverConfig.publicCredentialsSaveEnabled = data.state.serverConfig.publicCredentialsSaveEnabled; - state.serverConfig.customConnectionsEnabled = data.state.serverConfig.customConnectionsEnabled; - - state.navigatorConfig = { ...data.state.navigatorConfig }; - }; - private save: IExecutorHandler = async (data, contexts) => { const validation = contexts.getContext(serverConfigValidationContext); - const state = contexts.getContext(serverConfigStateContext); if (!validation.getState()) { return; } try { - await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings: state.navigatorConfig }); + await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings: data.state.navigatorConfig }); if (!data.configurationWizard || data.finish) { - await this.graphQLService.sdk.configureServer({ - configuration: state.serverConfig, - }); - await this.serverConfigResource.update(); - this.usersResource.refreshAllLazy(); + await this.serverConfigResource.save(); } } catch (exception) { this.notificationService.logException(exception, 'Can\'t save server configuration'); @@ -267,8 +234,6 @@ export function serverConfigValidationContext(): IValidationStatusContext { export function serverConfigStateContext(): IServerConfigurationPageState { return { navigatorConfig: { ...DEFAULT_NAVIGATOR_VIEW_SETTINGS }, - serverConfig: { - enabledAuthProviders: [], - }, + serverConfig: {}, }; } diff --git a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx index a7e3e5a71d..f2c971d052 100644 --- a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; import { AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; -import { BASE_CONTAINERS_STYLES, Container, Group, GroupTitle, Loader, PlaceholderComponent, SwitchNew, useDataResource } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, Container, Group, GroupTitle, Loader, PlaceholderComponent, SwitchNew, useMapResource } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; import type { IConfigurationPlaceholderProps } from '@cloudbeaver/plugin-administration'; @@ -18,12 +18,13 @@ import type { IConfigurationPlaceholderProps } from '@cloudbeaver/plugin-adminis import { ServerConfigurationAdminForm } from './ServerConfigurationAdminForm'; export const AuthenticationProviders: PlaceholderComponent = observer(function AuthenticationProviders({ - serverConfig, + state: { serverConfig }, configurationWizard, }) { - const providers = useDataResource(AuthProvidersResource, undefined); + const providers = useMapResource(AuthProvidersResource, AuthProvidersResource.keyAll); const translate = useTranslate(); const styles = useStyles(BASE_CONTAINERS_STYLES); + const disabled = providers.data.length === 1 && !providers.resource.has(AUTH_PROVIDER_LOCAL_ID); return styled(styles)( @@ -58,7 +59,7 @@ export const AuthenticationProviders: PlaceholderComponent !disabled && styled(styles)( <> {translate('administration_configuration_wizard_configuration_authentication_provider')} - {providers.data.map(provider => ( + {providers.data.map(provider => provider && ( = async (data, contexts) => { - const providers = await this.authProvidersResource.load(); + const providers = await this.authProvidersResource.loadAll(); const disabled = providers.length === 1 && !this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); if (disabled) { - data.state.serverConfig.enabledAuthProviders = providers.map(provider => provider.id); + data.state.serverConfig.enabledAuthProviders = [...this.authProvidersResource.keys]; data.state.serverConfig.authenticationEnabled = true; } @@ -71,26 +71,20 @@ export class ServerConfigurationAuthenticationBootstrap extends Bootstrap { }; private prepareConfig: IExecutorHandler = async (data, contexts) => { - const providers = await this.authProvidersResource.load(); + const providers = await this.authProvidersResource.loadAll(); const disabled = providers.length === 1 && !this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); - const state = contexts.getContext(serverConfigStateContext); - - state.serverConfig.anonymousAccessEnabled = data.state.serverConfig.anonymousAccessEnabled; - state.serverConfig.authenticationEnabled = data.state.serverConfig.authenticationEnabled; - state.serverConfig.enabledAuthProviders = data.state.serverConfig.enabledAuthProviders; - if (disabled) { - state.serverConfig.enabledAuthProviders = providers.map(provider => provider.id); - state.serverConfig.authenticationEnabled = true; + data.state.serverConfig.enabledAuthProviders = [...this.authProvidersResource.keys]; + data.state.serverConfig.authenticationEnabled = true; } if ( - data.configurationWizard - && state.serverConfig.enabledAuthProviders?.includes(AUTH_PROVIDER_LOCAL_ID) + !data.configurationWizard + || !data.state.serverConfig.enabledAuthProviders?.includes(AUTH_PROVIDER_LOCAL_ID) ) { - state.serverConfig.adminName = data.state.serverConfig.adminName; - state.serverConfig.adminPassword = data.state.serverConfig.adminPassword; + data.state.serverConfig.adminName = undefined; + data.state.serverConfig.adminPassword = undefined; } }; diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts index 44257bab9b..9b5b04f7aa 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts @@ -40,7 +40,7 @@ export class UsersAdministrationController implements IInitializableController { } get isLocalProviderAvailable(): boolean { - return this.authProvidersResource.data.some(({ id }) => id === AUTH_PROVIDER_LOCAL_ID); + return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); } get isLoading(): boolean { @@ -65,7 +65,7 @@ export class UsersAdministrationController implements IInitializableController { } init(): void { - this.authProvidersResource.load(); + this.authProvidersResource.loadAll(); } update = async () => { diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 467e583719..a43564f9c8 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -11,7 +11,8 @@ import { AppScreenService } from '@cloudbeaver/core-app'; import { AppAuthService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import type { IExecutorHandler } from '@cloudbeaver/core-executor'; +import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; @@ -29,7 +30,8 @@ export class AuthenticationService extends Bootstrap { private notificationService: NotificationService, private readonly administrationScreenService: AdministrationScreenService, private readonly authProviderService: AuthProviderService, - private readonly authProvidersResource: AuthProvidersResource + private readonly authProvidersResource: AuthProvidersResource, + private readonly sessionDataResource: SessionDataResource, ) { super(); this.authPromise = null; @@ -73,11 +75,11 @@ export class AuthenticationService extends Bootstrap { } register(): void { - this.appAuthService.auth.addPostHandler(state => { - if (!state) { - this.requireAuthentication(); - } - }); + this.sessionDataResource.beforeLoad.addHandler( + ExecutorInterrupter.interrupter(() => this.appAuthService.isAuthNeeded()) + ); + this.sessionDataResource.beforeLoad.addPostHandler(() => { this.requireAuthentication(); }); + this.appScreenService.activation.addHandler(() => this.requireAuthentication()); this.administrationScreenService.ensurePermissions.addHandler(async () => { const userInfo = await this.userInfoResource.load(); @@ -99,7 +101,7 @@ export class AuthenticationService extends Bootstrap { return; } - await this.authProvidersResource.load(); + await this.authProvidersResource.loadAll(); await this.userInfoResource.load(); if (!this.authProvidersResource.has(data.subType ?? data.type)) { diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 08c85f1331..85ef3b6a85 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -9,10 +9,11 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; +import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { SubmittingForm, ErrorMessage, TabsState, TabList, Tab, TabTitle, Loader } from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; +import { useController, useService } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; @@ -77,6 +78,10 @@ const styles = composes( bottom: 0; padding: 8px 24px; } + auth-token-info-message { + composes: theme-typography--caption from global; + padding: 8px 24px; + } ` ); @@ -85,6 +90,7 @@ export const AuthDialog: DialogComponent = observer(functio options, rejectDialog, }) { + const userInfo = useService(UserInfoResource); const controller = useController(AuthDialogController, rejectDialog); const translate = useTranslate(); @@ -98,18 +104,24 @@ export const AuthDialog: DialogComponent = observer(functio - {controller.providers.map(provider => ( - controller.selectProvider(provider.id)} - > - {provider.label} - - ))} - + icon={controller.provider?.icon} + header={( + <> + {userInfo.data !== null && {translate('authentication_request_token')}} + {showTabs && ( + + {controller.providers.map(provider => ( + controller.selectProvider(provider.id)} + > + {provider.label} + + ))} + + )} + )} footer={( provider.id === providerId) || null; + this.provider = this.authProvidersResource.get(providerId) || null; this.credentials = {}; }; @@ -94,7 +92,7 @@ export class AuthDialogController implements IInitializableController, IDestruct private async loadProviders() { try { - await this.authProvidersResource.load(); + await this.authProvidersResource.loadAll(); if (this.providers.length > 0) { this.provider = this.providers[0]; } diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index 08a43fad59..b2ca22123f 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -2,6 +2,7 @@ export default [ ['authentication_login_dialog_title', 'Authentication'], ['authentication_login', 'Login'], ['authentication_logout', 'Logout'], + ['authentication_request_token', 'Additional authentication is required'], ['authentication_administration_user_connections_user_add', 'User Creation'], ['authentication_administration_user_connections_user_new', 'New user'], ['authentication_administration_user_connections_access_load_fail', 'User\'s granted connections loading failed'], diff --git a/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts b/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts index a7faf70e95..a0823e9d72 100644 --- a/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts +++ b/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts @@ -6,10 +6,9 @@ * you may not use this file except in compliance with the License. */ -import { AppAuthService } from '@cloudbeaver/core-authentication'; import { Connection, ConnectionsResource } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { EPermission, PermissionsService } from '@cloudbeaver/core-root'; +import { EPermission, PermissionsService, SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, CachedDataResource } from '@cloudbeaver/core-sdk'; @injectable() @@ -18,11 +17,11 @@ export class TemplateConnectionsResource extends CachedDataResource this.markOutdated()); - appAuthService.auth.addHandler(() => this.markOutdated()); + sessionDataResource.onDataOutdated.addHandler(() => this.markOutdated()); } isLoaded(): boolean { diff --git a/webapp/packages/plugin-data-export/src/Bootstrap.ts b/webapp/packages/plugin-data-export/src/Bootstrap.ts index de48a9905c..dcd7f37477 100644 --- a/webapp/packages/plugin-data-export/src/Bootstrap.ts +++ b/webapp/packages/plugin-data-export/src/Bootstrap.ts @@ -6,15 +6,19 @@ * you may not use this file except in compliance with the License. */ -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap as B, injectable } from '@cloudbeaver/core-di'; import { DataExportMenuService } from './DataExportMenuService'; @injectable() -export class Bootstrap { - constructor(private dataExportMenuService: DataExportMenuService) { } +export class Bootstrap extends B { + constructor(private dataExportMenuService: DataExportMenuService) { + super(); + } - bootstrap() { + register(): void { this.dataExportMenuService.register(); } + + load(): void {} } diff --git a/webapp/packages/plugin-data-export/src/manifest.ts b/webapp/packages/plugin-data-export/src/manifest.ts index 735f61678f..f5c7b27694 100644 --- a/webapp/packages/plugin-data-export/src/manifest.ts +++ b/webapp/packages/plugin-data-export/src/manifest.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { IServiceInjector, PluginManifest } from '@cloudbeaver/core-di'; +import type { PluginManifest } from '@cloudbeaver/core-di'; import { Bootstrap } from './Bootstrap'; import { DataExportMenuService } from './DataExportMenuService'; @@ -21,16 +21,11 @@ export const manifest: PluginManifest = { }, providers: [ + Bootstrap, DataExportMenuService, DataExportService, DataExportProcessService, DataTransferProcessorsResource, LocaleService, ], - - async initialize(services: IServiceInjector) { - services - .resolveServiceByClass(Bootstrap) - .bootstrap(); - }, }; diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts index 6b398bd808..df357133db 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts @@ -6,22 +6,27 @@ * you may not use this file except in compliance with the License. */ -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ITabBuilder, ObjectFoldersService } from '@cloudbeaver/plugin-object-viewer'; import { DdlViewerTabService } from './DdlViewerTabService'; @injectable() -export class DdlViewerBootstrap { - constructor(private objectFoldersService: ObjectFoldersService, - private ddlViewerTabService: DdlViewerTabService) { +export class DdlViewerBootstrap extends Bootstrap { + constructor( + private objectFoldersService: ObjectFoldersService, + private ddlViewerTabService: DdlViewerTabService + ) { + super(); } - bootstrap() { + register(): void { const tabBuilder: ITabBuilder = { build: nodeId => this.ddlViewerTabService.buildTab(nodeId), }; this.objectFoldersService.registerTabConstructor(tabBuilder); } + + load(): void {} } diff --git a/webapp/packages/plugin-ddl-viewer/src/manifest.ts b/webapp/packages/plugin-ddl-viewer/src/manifest.ts index df6505eae5..8c024f01e5 100644 --- a/webapp/packages/plugin-ddl-viewer/src/manifest.ts +++ b/webapp/packages/plugin-ddl-viewer/src/manifest.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { IServiceInjector, PluginManifest } from '@cloudbeaver/core-di'; +import type { PluginManifest } from '@cloudbeaver/core-di'; import { DdlViewerBootstrap } from './DdlViewerBootstrap'; import { DdlViewerService } from './DdlViewerService'; @@ -18,12 +18,8 @@ export const manifest: PluginManifest = { }, providers: [ + DdlViewerBootstrap, DdlViewerTabService, DdlViewerService, ], - - initialize(services: IServiceInjector): void { - const service = services.resolveServiceByClass(DdlViewerBootstrap); - service.bootstrap(); - }, }; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerBootstrap.ts b/webapp/packages/plugin-object-viewer/src/ObjectViewerBootstrap.ts index 3f11d8829d..72bdb4794c 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerBootstrap.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerBootstrap.ts @@ -6,20 +6,24 @@ * you may not use this file except in compliance with the License. */ -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ObjectPropertiesPageService } from './ObjectPropertiesPage/ObjectPropertiesPageService'; import { ObjectViewerTabService } from './ObjectViewerTabService'; @injectable() -export class ObjectViewerBootstrap { +export class ObjectViewerBootstrap extends Bootstrap { constructor( private objectViewerTabService: ObjectViewerTabService, private objectPropertiesPageService: ObjectPropertiesPageService - ) { } + ) { + super(); + } - bootstrap() { + register(): void { this.objectViewerTabService.registerTabHandler(); this.objectPropertiesPageService.registerDBObjectPage(); } + + load(): void {} } diff --git a/webapp/packages/plugin-object-viewer/src/manifest.ts b/webapp/packages/plugin-object-viewer/src/manifest.ts index 56dbca742a..a2073f1e7b 100644 --- a/webapp/packages/plugin-object-viewer/src/manifest.ts +++ b/webapp/packages/plugin-object-viewer/src/manifest.ts @@ -22,6 +22,7 @@ export const manifest: PluginManifest = { info: { name: 'Object Viewer Plugin' }, providers: [ + ObjectViewerBootstrap, ObjectPropertiesPageService, ObjectFoldersService, ObjectViewerTabService, @@ -31,11 +32,4 @@ export const manifest: PluginManifest = { DBObjectPageService, LocaleService, ], - - async initialize(injector: IServiceInjector) { - injector - .resolveServiceByClass(ObjectViewerBootstrap) - .bootstrap(); - }, - }; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts index ecc07d7396..18432bfd20 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts @@ -15,7 +15,7 @@ import { isObjectCatalogProvider, isObjectSchemaProvider } from '@cloudbeaver/core-app'; import { isConnectionProvider } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuContext } from '@cloudbeaver/core-dialogs'; import { ExtensionUtils } from '@cloudbeaver/core-extensions'; import { ActiveViewService } from '@cloudbeaver/core-view'; @@ -23,16 +23,18 @@ import { ActiveViewService } from '@cloudbeaver/core-view'; import { SqlEditorNavigatorService } from './SqlEditorNavigatorService'; @injectable() -export class SqlEditorBootstrap { +export class SqlEditorBootstrap extends Bootstrap { constructor( private mainMenuService: MainMenuService, private contextMenuService: ContextMenuService, private sqlEditorNavigatorService: SqlEditorNavigatorService, private connectionSchemaManagerService: ConnectionSchemaManagerService, private activeViewService: ActiveViewService - ) { } + ) { + super(); + } - async bootstrap() { + register(): void { this.mainMenuService.registerRootItem( { id: 'sql-editor', @@ -58,6 +60,8 @@ export class SqlEditorBootstrap { }); } + load(): void {} + private isSQLEntryDisabled() { const activeView = this.activeViewService.view; if (activeView) { diff --git a/webapp/packages/plugin-sql-editor/src/manifest.ts b/webapp/packages/plugin-sql-editor/src/manifest.ts index 4a851b7d97..f23d088ece 100644 --- a/webapp/packages/plugin-sql-editor/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor/src/manifest.ts @@ -23,6 +23,7 @@ export const sqlEditorPluginManifest: PluginManifest = { }, providers: [ + SqlEditorBootstrap, SqlDialectInfoService, SqlEditorTabService, SqlResultTabsService, @@ -31,8 +32,4 @@ export const sqlEditorPluginManifest: PluginManifest = { SqlEditorNavigatorService, LocaleService, ], - - initialize(services): void { - services.resolveServiceByClass(SqlEditorBootstrap).bootstrap(); - }, };