mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-4067 Migrate Connection form to form template (#3288)
* CB-4067 adds connection form state, service and options part * CB-4067 adds connection origin info tab to new form * CB-4067 adds SSH part * CB-4067 adds driver properties part * CB-4067 changes driverId and submitType location for connection form * CB-4067 adds SSL part * CB-4067 adds connection form access part * CB-4067 uses new form parts for connection form * CB-4067 resets form on open * CB-4067 fixes configure case for connection form * CB-4067 adds selected driver on edit mode form * CB-4067 reloads form when it is needed * CB-4067 adds button actions for the form * CB-4067 adds notifications on form save/error * CB-4067 fixes form ssl saving and driver properties * CB-4067 refactors some parts * CB-4067 refactors form options part so it works fine * CB-4067 fixes driver properties part * CB-4067 fixes SSH part * CB-4067 fixes SSL part * CB-4067 fixes Access part * CB-4067 refactors origin tab * CB-4067 refactors shared credentials part * CB-4067 fixes: secret providers, SSH, SSL, test - features * CB-4067 removes refactor postfix for new state, service and types * CB-4067 fixes connection test * CB-4067 cleanup * CB-4067 gets back ask credentials window and removes connectionCredentialsStateContext * CB-4067 adds translation to test connection success message * CB-4067 fixes ssh and ssl adding to networkHandlers. also fixes extra request for driver properties * CB-4067 fixes host and port for switching connections * CB-4067 removes redundant code * CB-4067 fixes exception error on retest connection + fixes SSH field key saved incorrect prop * CB-4067 cleanup testContext info * CB-4076 fixes SSH and SSL ask credentials and parameters saving for both: test and submit connection create/edit action * CB-4067 fixes access tab reminder * CB-4067 fixes handling of info message for cloud connection origin tabs * CB-4067 fixes checkboxes behavior for TE shared credentials feature * CB-4067 fixes create connection template popup SSH form * CB-4067 fixes notification for closing ask credentials popup if before we had an error of test submit * CB-6115 adds dispose API for forms * CB-6115 adds git workspace helper extension for repo recommendations * CB-4067 cleanup * CB-6115 pr fixes * CB-4067 gets back behavior with dispose connection form state * CB-4067 cleanup * СB-4067 splits states to ui and form part in access form part * СB-4067 pr fixes * CB-4067 removes config from form state * CB-4067 removes console warnings + clean up * CB-4067 moves synchronization mechanisms from ConnectionFormState to ConnectionOptionsFormPart * CB-4067 fixes bug with incorrect host or port during search or changing driverId during connection creation * CB-4067 fixes opening connection forms from different entry points so it shows consistent initial data * СB-4067 brings back saving the form * CB-4067 fixes logic of showing popup askCredentials * CB-4067 fixes origin tab name so it shows correctly + fixes unneeded loaders of resources * CB-4067 reverts isOutdated * CB-4067 fixes loading of driver properties * CB-4067 cleanup for connection access part * Revert "CB-4067 reverts isOutdated" This reverts commitecb195ee2a. * CB-4067 adds isOutdated to the parts * CB-4067 fixes origin tab info message * CB-4067 fixes driver properties state + origin info status message shows on load * CB-4067 fixes password autofill during form save * CB-4067 fixes linter * CB-4067 fixes linter * CB-4067 cleanup + fixes bug with search database host,name fields * CB-4067 linter fix * CB-4067 pr fixes * CB-4067 removes unused post handler for formstate loaded task * CB-4067 small pr fixes * CB-4067 adds setDriver + handles name autocomplete * chore: review * CB-4067 connection part correct loading * CB-4067 adds formState.state.connectionId default value * refactor: pr review * CB-4067 fixes bug with infinite loading of the origin info details * CB-4067 fix: allows askCredentials to edit fields + does not affects part state * CB-4067 resets driver properties if changed driverId * CB-4067 updates yarn lock for common-react * CB-4067 lint fix * CB-6265 fix: do not include index files for import/export eslint rules * CB-4067 fixes behavior for aksCredentials so it acts correctly during testing/saving/editing inputs for the form * CB-4067 cleanup * CB-4067 fixes connection templates initial loaded state * CB-4067 pr fixes: dialog credentials; adds todo to the ssh tab; eslint revert; cleanup * CB-4067 makes askCredentials to be part state independent * CB-4067 qa fixes: test translation + removes db name form connection name * CB-4067 adds readonly state for connection form * CB-4067 fixes application freezes on disconnect * CB-4067 fixes empty properties when switching cloud connections * CB-4067 connection is created -> connection was created * CB-4067 connection is updated -> connection was updated * CB-4067 fixes outdated logic for parts * CB-4067 build fix * CB-4067 fix: do not show askCredential popup for drivers with only anonymous access * CB-4067 removes includeCredentialsSaved * CB-4067 removes includeAuthProperties * CB-4067 removes customIncludeOptions * CB-4067 removes includeProperties * CB-4067 removes includeProviderProperties * CB-4067 removes includeNetworkHandlers * CB-4067 refactors isOutdated for originInfoPart and AccessPart * CB-4067 removes savedCredentials resource + updates CustomOptionsResource + reuses new include resources in connection form components * CB-4067 reverts spreading of new connectionInfoResources * CB-4067 fixes sharedCreds checkbox default value on form switch * CB-4067 closes connection form when layout closes * CB-4067 disposes connection form on change * CB-4067 handles closing of the connection form correctly * Revert "CB-4067 handles closing of the connection form correctly" This reverts commit3a6f1a6a0a. * CB-4067 handles closing of the connection form correctly [2] * CB-4067 fits info messages in one line * CB-4067 presaves isChanged state for form part before applying initialState to prevent bugs * CB-4067 fixes length of the error message for connection form top bar --------- Co-authored-by: Alexey <wrouds@gmail.com> Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
co-authored by
Alexey
mr-anton-t
Daria Marutkina
parent
08e6d9631f
commit
4dd8ddbefd
Vendored
-8
@@ -92,14 +92,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Update TS references CE",
|
||||
"type": "shell",
|
||||
"command": "yarn run update-ts-references",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/webapp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Clean CE",
|
||||
"type": "shell",
|
||||
|
||||
@@ -335,6 +335,16 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@dbeaver/jdbc-uri-parser@workspace:../common-typescript/@dbeaver/jdbc-uri-parser":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/jdbc-uri-parser@workspace:../common-typescript/@dbeaver/jdbc-uri-parser"
|
||||
dependencies:
|
||||
"@dbeaver/tsconfig": "workspace:^"
|
||||
rimraf: "npm:^6"
|
||||
typescript: "npm:^5"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@dbeaver/prettier-config@workspace:../common-typescript/@dbeaver/prettier-config, @dbeaver/prettier-config@workspace:^":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/prettier-config@workspace:../common-typescript/@dbeaver/prettier-config"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# @dbeaver/jdbc-uri-parser
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@dbeaver/jdbc-uri-parser",
|
||||
"type": "module",
|
||||
"sideEffects": [],
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./*": "./lib/*.js"
|
||||
},
|
||||
"scripts": {
|
||||
"clear": "rimraf lib"
|
||||
},
|
||||
"files": [
|
||||
"package.json",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"lib",
|
||||
"!lib/**/*.d.ts.map",
|
||||
"!lib/**/*.test.js",
|
||||
"!lib/**/*.test.d.ts",
|
||||
"!lib/**/*.test.d.ts.map",
|
||||
"!lib/**/*.test.js.map",
|
||||
"!lib/tests",
|
||||
"!.tsbuildinfo"
|
||||
],
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"devDependencies": {
|
||||
"@dbeaver/tsconfig": "workspace:^",
|
||||
"rimraf": "^6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface JdbcUriBase {
|
||||
raw: string;
|
||||
subprotocol: string;
|
||||
subname: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { JdbcUriBase } from './JdbcUriBase.js';
|
||||
|
||||
export interface JdbcUriCommon extends JdbcUriBase {
|
||||
host?: string;
|
||||
port?: string;
|
||||
database?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
queryParams: Record<string, string>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './JdbcUriBase.js';
|
||||
export * from './JdbcUriCommon.js';
|
||||
export * from './parseJdbcUri.js';
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { JdbcUriBase } from './JdbcUriBase.js';
|
||||
import type { JdbcUriCommon } from './JdbcUriCommon.js';
|
||||
|
||||
export type JdbcUri = JdbcUriCommon;
|
||||
|
||||
// Helper function to parse query parameters safely
|
||||
function parseQueryParams(query?: string): Record<string, string> {
|
||||
if (!query) return {};
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
try {
|
||||
for (const param of query.split('&')) {
|
||||
const [key, value] = param.split('=');
|
||||
if (key) {
|
||||
params[key] = value ? decodeURIComponent(value) : '';
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export function parseJdbcUri(jdbcUrl: string | undefined | null): JdbcUri {
|
||||
if (!jdbcUrl || typeof jdbcUrl !== 'string') {
|
||||
return {
|
||||
raw: String(jdbcUrl || ''),
|
||||
subprotocol: '',
|
||||
subname: '',
|
||||
queryParams: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (!jdbcUrl.startsWith('jdbc:')) {
|
||||
return {
|
||||
raw: jdbcUrl,
|
||||
subprotocol: '',
|
||||
subname: jdbcUrl,
|
||||
queryParams: {},
|
||||
};
|
||||
}
|
||||
|
||||
const raw = jdbcUrl;
|
||||
const match = jdbcUrl.match(/^jdbc:([^:]+):(.*)$/);
|
||||
|
||||
if (!match) {
|
||||
return {
|
||||
raw: jdbcUrl,
|
||||
subprotocol: '',
|
||||
subname: jdbcUrl.substring(5),
|
||||
queryParams: {},
|
||||
};
|
||||
}
|
||||
|
||||
const [, subprotocol, subname] = match as [string, string, string];
|
||||
const base: JdbcUriBase = { raw, subprotocol, subname };
|
||||
|
||||
// Protocol-specific handling
|
||||
|
||||
// SQLite: jdbc:sqlite:/path/to/file.db
|
||||
if (subprotocol === 'sqlite') {
|
||||
return {
|
||||
...base,
|
||||
database: subname,
|
||||
queryParams: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Oracle Thin: jdbc:oracle:thin:@host:port:sid
|
||||
if (subprotocol === 'oracle' && subname.startsWith('thin:@')) {
|
||||
const match = subname.match(/^thin:@([^:]+):(\d+):(.+)$/);
|
||||
if (match) {
|
||||
const [, host, port, database] = match;
|
||||
return { ...base, host, port, database, queryParams: {} };
|
||||
}
|
||||
return { ...base, queryParams: {} };
|
||||
}
|
||||
|
||||
// Handle URL-like formats (most JDBC URLs)
|
||||
if (subname.startsWith('//')) {
|
||||
// Extract query string if present
|
||||
const queryMatch = subname.match(/\?(.+)$/);
|
||||
const queryString = queryMatch?.[1];
|
||||
const queryParams = parseQueryParams(queryString);
|
||||
|
||||
// Strip query string for further parsing
|
||||
const subnameWithoutQuery = queryString ? subname.substring(0, subname.length - queryString.length - 1) : subname;
|
||||
|
||||
// Check for template placeholders
|
||||
if (/\{[^{}]+\}/.test(subnameWithoutQuery)) {
|
||||
const hostMatch = subnameWithoutQuery.match(/\/\/([^:/]+|\{[^{}]+\})/);
|
||||
const portMatch = subnameWithoutQuery.match(/\/\/(?:[^:/]+|\{[^{}]+\})(?::([^/]+|\{[^{}]+\}))?/);
|
||||
const databaseMatch = subnameWithoutQuery.match(/\/([^?]*)/);
|
||||
|
||||
const host = hostMatch?.[1];
|
||||
const port = portMatch?.[1];
|
||||
const database = databaseMatch?.[1];
|
||||
|
||||
return {
|
||||
...base,
|
||||
host: host?.startsWith('{') ? undefined : host,
|
||||
port: port?.startsWith('{') ? undefined : port,
|
||||
database: database?.startsWith('{') ? undefined : database,
|
||||
queryParams,
|
||||
};
|
||||
}
|
||||
|
||||
// Try standard URL parsing
|
||||
try {
|
||||
const fakeUrl = new URL(`${subprotocol}:${subnameWithoutQuery}`);
|
||||
return {
|
||||
...base,
|
||||
host: fakeUrl.hostname || undefined,
|
||||
port: fakeUrl.port || undefined,
|
||||
database: fakeUrl.pathname?.slice(1) || undefined,
|
||||
username: fakeUrl.username || undefined,
|
||||
password: fakeUrl.password || undefined,
|
||||
queryParams,
|
||||
};
|
||||
} catch {
|
||||
// Fallback regex parsing for malformed URLs
|
||||
const regex = /^\/\/([^:/?#]+)?(?::(\d+))?(?:\/([^?]*))?/;
|
||||
const match = subnameWithoutQuery.match(regex);
|
||||
const [, host, port, database] = match || [];
|
||||
|
||||
return {
|
||||
...base,
|
||||
host,
|
||||
port,
|
||||
database,
|
||||
queryParams,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Any other format - treat subname as database
|
||||
return {
|
||||
...base,
|
||||
database: subname,
|
||||
queryParams: {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "@dbeaver/tsconfig/tsconfig.webview.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
||||
"rootDir": "src",
|
||||
"rootDirs": [
|
||||
"src"
|
||||
],
|
||||
"outDir": "lib",
|
||||
"incremental": true,
|
||||
"composite": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -107,6 +107,16 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@dbeaver/jdbc-uri-parser@workspace:@dbeaver/jdbc-uri-parser":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/jdbc-uri-parser@workspace:@dbeaver/jdbc-uri-parser"
|
||||
dependencies:
|
||||
"@dbeaver/tsconfig": "workspace:^"
|
||||
rimraf: "npm:^6"
|
||||
typescript: "npm:^5"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@dbeaver/prettier-config@workspace:@dbeaver/prettier-config, @dbeaver/prettier-config@workspace:^":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@dbeaver/prettier-config@workspace:@dbeaver/prettier-config"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { runInAction, toJS } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { type DatabaseConnectionAuthPropertiesFragment, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { schemaValidationError } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CONNECTION_INFO_PARAM_SCHEMA, type IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA.js';
|
||||
import {
|
||||
ConnectionInfoActiveProjectKey,
|
||||
ConnectionInfoProjectKey,
|
||||
ConnectionInfoResource,
|
||||
createConnectionParam,
|
||||
isConnectionInfoParamEqual,
|
||||
} from './ConnectionInfoResource.js';
|
||||
import { parseConnectionKey } from './parseConnectionKey.js';
|
||||
|
||||
export type ConnectionInfoAuthProperties = DatabaseConnectionAuthPropertiesFragment;
|
||||
|
||||
// TODO reuse it everywhere where include "includeAuthProperties" is used
|
||||
@injectable()
|
||||
export class ConnectionInfoAuthPropertiesResource extends CachedMapResource<IConnectionInfoParams, ConnectionInfoAuthProperties> {
|
||||
constructor(
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly graphQLService: GraphQLService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.aliases.add(ConnectionInfoProjectKey, param => resourceKeyList(this.keys.filter(key => param.options.projectIds.includes(key.projectId))));
|
||||
|
||||
this.aliases.add(ConnectionInfoActiveProjectKey, () =>
|
||||
resourceKeyList(this.keys.filter(key => projectsService.activeProjects.some(({ id }) => id === key.projectId))),
|
||||
);
|
||||
|
||||
this.sync(this.connectionInfoResource);
|
||||
this.connectionInfoResource.onItemDelete.addHandler(this.delete.bind(this));
|
||||
}
|
||||
|
||||
protected async loader(
|
||||
originalKey: ResourceKey<IConnectionInfoParams>,
|
||||
_: any,
|
||||
refresh?: boolean,
|
||||
): Promise<Map<IConnectionInfoParams, ConnectionInfoAuthProperties>> {
|
||||
const connectionsList: ConnectionInfoAuthProperties[] = [];
|
||||
let removedConnections: IConnectionInfoParams[] = [];
|
||||
const parsed = parseConnectionKey({
|
||||
originalKey,
|
||||
aliases: this.aliases,
|
||||
isOutdated: this.isOutdated.bind(this),
|
||||
activeProjects: this.projectsService.activeProjects,
|
||||
refresh,
|
||||
});
|
||||
|
||||
let { projectId } = parsed;
|
||||
const { projectIds, key } = parsed;
|
||||
|
||||
await ResourceKeyUtils.forEachAsync(key, async connectionKey => {
|
||||
let connectionId: string | undefined;
|
||||
if (!isResourceAlias(connectionKey)) {
|
||||
projectId = connectionKey.projectId;
|
||||
connectionId = connectionKey.connectionId;
|
||||
}
|
||||
|
||||
const { connections } = await this.graphQLService.sdk.getUserConnectionsAuthProperties({
|
||||
projectId,
|
||||
connectionId,
|
||||
projectIds,
|
||||
});
|
||||
|
||||
if (connectionId && !connections.some(connection => connection.id === connectionId)) {
|
||||
throw new Error(`Connection is not found (${connectionId})`);
|
||||
}
|
||||
|
||||
connectionsList.push(...connections);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (isResourceAlias(key)) {
|
||||
removedConnections = ResourceKeyUtils.toList(this.aliases.transformToKey(key)).filter(
|
||||
filterKey => !connectionsList.some(connection => isConnectionInfoParamEqual(filterKey, createConnectionParam(connection))),
|
||||
);
|
||||
}
|
||||
|
||||
this.delete(resourceKeyList(removedConnections));
|
||||
const keys = resourceKeyList(connectionsList.map(createConnectionParam));
|
||||
this.set(keys, connectionsList);
|
||||
});
|
||||
|
||||
return this.data;
|
||||
}
|
||||
|
||||
override isKeyEqual(param: IConnectionInfoParams, second: IConnectionInfoParams): boolean {
|
||||
return isConnectionInfoParamEqual(param, second);
|
||||
}
|
||||
|
||||
protected validateKey(key: IConnectionInfoParams): boolean {
|
||||
const parse = CONNECTION_INFO_PARAM_SCHEMA.safeParse(toJS(key));
|
||||
if (!parse.success) {
|
||||
this.logger.warn(`Invalid resource key ${(schemaValidationError(parse.error).toString(), { prefix: null })}`);
|
||||
}
|
||||
return parse.success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { runInAction, toJS } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { type DatabaseConnectionCustomOptionsFragment, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { schemaValidationError } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CONNECTION_INFO_PARAM_SCHEMA, type IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA.js';
|
||||
import {
|
||||
ConnectionInfoActiveProjectKey,
|
||||
ConnectionInfoProjectKey,
|
||||
ConnectionInfoResource,
|
||||
createConnectionParam,
|
||||
isConnectionInfoParamEqual,
|
||||
} from './ConnectionInfoResource.js';
|
||||
import { parseConnectionKey } from './parseConnectionKey.js';
|
||||
|
||||
export type ConnectionInfoCustomOptions = DatabaseConnectionCustomOptionsFragment;
|
||||
|
||||
// TODO reuse it everywhere where include "customIncludeOptions" is used
|
||||
@injectable()
|
||||
export class ConnectionInfoCustomOptionsResource extends CachedMapResource<IConnectionInfoParams, ConnectionInfoCustomOptions> {
|
||||
constructor(
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly graphQLService: GraphQLService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.aliases.add(ConnectionInfoProjectKey, param => resourceKeyList(this.keys.filter(key => param.options.projectIds.includes(key.projectId))));
|
||||
|
||||
this.aliases.add(ConnectionInfoActiveProjectKey, () =>
|
||||
resourceKeyList(this.keys.filter(key => projectsService.activeProjects.some(({ id }) => id === key.projectId))),
|
||||
);
|
||||
|
||||
this.sync(this.connectionInfoResource);
|
||||
this.connectionInfoResource.onItemDelete.addHandler(this.delete.bind(this));
|
||||
}
|
||||
|
||||
protected async loader(
|
||||
originalKey: ResourceKey<IConnectionInfoParams>,
|
||||
_: any,
|
||||
refresh?: boolean,
|
||||
): Promise<Map<IConnectionInfoParams, ConnectionInfoCustomOptions>> {
|
||||
const connectionsList: ConnectionInfoCustomOptions[] = [];
|
||||
let removedConnections: IConnectionInfoParams[] = [];
|
||||
const parsed = parseConnectionKey({
|
||||
originalKey,
|
||||
aliases: this.aliases,
|
||||
isOutdated: this.isOutdated.bind(this),
|
||||
activeProjects: this.projectsService.activeProjects,
|
||||
refresh,
|
||||
});
|
||||
|
||||
let { projectId } = parsed;
|
||||
const { projectIds, key } = parsed;
|
||||
|
||||
await ResourceKeyUtils.forEachAsync(key, async connectionKey => {
|
||||
let connectionId: string | undefined;
|
||||
if (!isResourceAlias(connectionKey)) {
|
||||
projectId = connectionKey.projectId;
|
||||
connectionId = connectionKey.connectionId;
|
||||
}
|
||||
|
||||
const { connections } = await this.graphQLService.sdk.getUserConnectionsCustomOptions({
|
||||
projectId,
|
||||
connectionId,
|
||||
projectIds,
|
||||
});
|
||||
|
||||
if (connectionId && !connections.some(connection => connection.id === connectionId)) {
|
||||
throw new Error(`Connection is not found (${connectionId})`);
|
||||
}
|
||||
|
||||
connectionsList.push(...connections);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (isResourceAlias(key)) {
|
||||
removedConnections = ResourceKeyUtils.toList(this.aliases.transformToKey(key)).filter(
|
||||
filterKey => !connectionsList.some(connection => isConnectionInfoParamEqual(filterKey, createConnectionParam(connection))),
|
||||
);
|
||||
}
|
||||
|
||||
this.delete(resourceKeyList(removedConnections));
|
||||
const keys = resourceKeyList(connectionsList.map(createConnectionParam));
|
||||
this.set(keys, connectionsList);
|
||||
});
|
||||
|
||||
return this.data;
|
||||
}
|
||||
|
||||
override isKeyEqual(param: IConnectionInfoParams, second: IConnectionInfoParams): boolean {
|
||||
return isConnectionInfoParamEqual(param, second);
|
||||
}
|
||||
|
||||
protected validateKey(key: IConnectionInfoParams): boolean {
|
||||
const parse = CONNECTION_INFO_PARAM_SCHEMA.safeParse(toJS(key));
|
||||
if (!parse.success) {
|
||||
this.logger.warn(`Invalid resource key ${(schemaValidationError(parse.error).toString(), { prefix: null })}`);
|
||||
}
|
||||
return parse.success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { runInAction, toJS } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { type DatabaseConnectionNetworkHandlersFragment, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { schemaValidationError } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CONNECTION_INFO_PARAM_SCHEMA, type IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA.js';
|
||||
import {
|
||||
ConnectionInfoActiveProjectKey,
|
||||
ConnectionInfoProjectKey,
|
||||
ConnectionInfoResource,
|
||||
createConnectionParam,
|
||||
isConnectionInfoParamEqual,
|
||||
} from './ConnectionInfoResource.js';
|
||||
import { parseConnectionKey } from './parseConnectionKey.js';
|
||||
|
||||
export type ConnectionInfoNetworkHandlers = DatabaseConnectionNetworkHandlersFragment;
|
||||
|
||||
// TODO reuse it everywhere where include "includeNetworkHandlers" is used
|
||||
@injectable()
|
||||
export class ConnectionInfoNetworkHandlersResource extends CachedMapResource<IConnectionInfoParams, ConnectionInfoNetworkHandlers> {
|
||||
constructor(
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly graphQLService: GraphQLService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.aliases.add(ConnectionInfoProjectKey, param => resourceKeyList(this.keys.filter(key => param.options.projectIds.includes(key.projectId))));
|
||||
|
||||
this.aliases.add(ConnectionInfoActiveProjectKey, () =>
|
||||
resourceKeyList(this.keys.filter(key => projectsService.activeProjects.some(({ id }) => id === key.projectId))),
|
||||
);
|
||||
|
||||
this.sync(this.connectionInfoResource);
|
||||
this.connectionInfoResource.onItemDelete.addHandler(this.delete.bind(this));
|
||||
}
|
||||
|
||||
protected async loader(
|
||||
originalKey: ResourceKey<IConnectionInfoParams>,
|
||||
_: any,
|
||||
refresh?: boolean,
|
||||
): Promise<Map<IConnectionInfoParams, ConnectionInfoNetworkHandlers>> {
|
||||
const connectionsList: ConnectionInfoNetworkHandlers[] = [];
|
||||
let removedConnections: IConnectionInfoParams[] = [];
|
||||
const parsed = parseConnectionKey({
|
||||
originalKey,
|
||||
aliases: this.aliases,
|
||||
isOutdated: this.isOutdated.bind(this),
|
||||
activeProjects: this.projectsService.activeProjects,
|
||||
refresh,
|
||||
});
|
||||
|
||||
let { projectId } = parsed;
|
||||
const { projectIds, key } = parsed;
|
||||
|
||||
await ResourceKeyUtils.forEachAsync(key, async connectionKey => {
|
||||
let connectionId: string | undefined;
|
||||
if (!isResourceAlias(connectionKey)) {
|
||||
projectId = connectionKey.projectId;
|
||||
connectionId = connectionKey.connectionId;
|
||||
}
|
||||
|
||||
const { connections } = await this.graphQLService.sdk.getUserConnectionsNetworkHandlers({
|
||||
projectId,
|
||||
connectionId,
|
||||
projectIds,
|
||||
});
|
||||
|
||||
if (connectionId && !connections.some(connection => connection.id === connectionId)) {
|
||||
throw new Error(`Connection is not found (${connectionId})`);
|
||||
}
|
||||
|
||||
connectionsList.push(...connections);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (isResourceAlias(key)) {
|
||||
removedConnections = ResourceKeyUtils.toList(this.aliases.transformToKey(key)).filter(
|
||||
filterKey => !connectionsList.some(connection => isConnectionInfoParamEqual(filterKey, createConnectionParam(connection))),
|
||||
);
|
||||
}
|
||||
|
||||
this.delete(resourceKeyList(removedConnections));
|
||||
const keys = resourceKeyList(connectionsList.map(createConnectionParam));
|
||||
this.set(keys, connectionsList);
|
||||
});
|
||||
|
||||
return this.data;
|
||||
}
|
||||
|
||||
override isKeyEqual(param: IConnectionInfoParams, second: IConnectionInfoParams): boolean {
|
||||
return isConnectionInfoParamEqual(param, second);
|
||||
}
|
||||
|
||||
protected validateKey(key: IConnectionInfoParams): boolean {
|
||||
const parse = CONNECTION_INFO_PARAM_SCHEMA.safeParse(toJS(key));
|
||||
if (!parse.success) {
|
||||
this.logger.warn(`Invalid resource key ${(schemaValidationError(parse.error).toString(), { prefix: null })}`);
|
||||
}
|
||||
return parse.success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { runInAction, toJS } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { type DatabaseConnectionPropertiesFragment, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { schemaValidationError } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CONNECTION_INFO_PARAM_SCHEMA, type IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA.js';
|
||||
import {
|
||||
ConnectionInfoActiveProjectKey,
|
||||
ConnectionInfoProjectKey,
|
||||
ConnectionInfoResource,
|
||||
createConnectionParam,
|
||||
isConnectionInfoParamEqual,
|
||||
} from './ConnectionInfoResource.js';
|
||||
import { parseConnectionKey } from './parseConnectionKey.js';
|
||||
|
||||
export type ConnectionInfoProperties = DatabaseConnectionPropertiesFragment;
|
||||
|
||||
// TODO reuse it everywhere where include "includeProperties" is used
|
||||
@injectable()
|
||||
export class ConnectionInfoPropertiesResource extends CachedMapResource<IConnectionInfoParams, ConnectionInfoProperties> {
|
||||
constructor(
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly graphQLService: GraphQLService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.aliases.add(ConnectionInfoProjectKey, param => resourceKeyList(this.keys.filter(key => param.options.projectIds.includes(key.projectId))));
|
||||
|
||||
this.aliases.add(ConnectionInfoActiveProjectKey, () =>
|
||||
resourceKeyList(this.keys.filter(key => projectsService.activeProjects.some(({ id }) => id === key.projectId))),
|
||||
);
|
||||
|
||||
this.sync(this.connectionInfoResource);
|
||||
this.connectionInfoResource.onItemDelete.addHandler(this.delete.bind(this));
|
||||
}
|
||||
|
||||
protected async loader(
|
||||
originalKey: ResourceKey<IConnectionInfoParams>,
|
||||
_: any,
|
||||
refresh?: boolean,
|
||||
): Promise<Map<IConnectionInfoParams, ConnectionInfoProperties>> {
|
||||
const connectionsList: ConnectionInfoProperties[] = [];
|
||||
let removedConnections: IConnectionInfoParams[] = [];
|
||||
const parsed = parseConnectionKey({
|
||||
originalKey,
|
||||
aliases: this.aliases,
|
||||
isOutdated: this.isOutdated.bind(this),
|
||||
activeProjects: this.projectsService.activeProjects,
|
||||
refresh,
|
||||
});
|
||||
|
||||
let { projectId } = parsed;
|
||||
const { projectIds, key } = parsed;
|
||||
|
||||
await ResourceKeyUtils.forEachAsync(key, async connectionKey => {
|
||||
let connectionId: string | undefined;
|
||||
if (!isResourceAlias(connectionKey)) {
|
||||
projectId = connectionKey.projectId;
|
||||
connectionId = connectionKey.connectionId;
|
||||
}
|
||||
|
||||
const { connections } = await this.graphQLService.sdk.getUserConnectionsProperties({
|
||||
projectId,
|
||||
connectionId,
|
||||
projectIds,
|
||||
});
|
||||
|
||||
if (connectionId && !connections.some(connection => connection.id === connectionId)) {
|
||||
throw new Error(`Connection is not found (${connectionId})`);
|
||||
}
|
||||
|
||||
connectionsList.push(...connections);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (isResourceAlias(key)) {
|
||||
removedConnections = ResourceKeyUtils.toList(this.aliases.transformToKey(key)).filter(
|
||||
filterKey => !connectionsList.some(connection => isConnectionInfoParamEqual(filterKey, createConnectionParam(connection))),
|
||||
);
|
||||
}
|
||||
|
||||
this.delete(resourceKeyList(removedConnections));
|
||||
const keys = resourceKeyList(connectionsList.map(createConnectionParam));
|
||||
this.set(keys, connectionsList);
|
||||
});
|
||||
|
||||
return this.data;
|
||||
}
|
||||
|
||||
override isKeyEqual(param: IConnectionInfoParams, second: IConnectionInfoParams): boolean {
|
||||
return isConnectionInfoParamEqual(param, second);
|
||||
}
|
||||
|
||||
protected validateKey(key: IConnectionInfoParams): boolean {
|
||||
const parse = CONNECTION_INFO_PARAM_SCHEMA.safeParse(toJS(key));
|
||||
if (!parse.success) {
|
||||
this.logger.warn(`Invalid resource key ${(schemaValidationError(parse.error).toString(), { prefix: null })}`);
|
||||
}
|
||||
return parse.success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { runInAction, toJS } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { type DatabaseConnectionProviderPropertiesFragment, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { schemaValidationError } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CONNECTION_INFO_PARAM_SCHEMA, type IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA.js';
|
||||
import {
|
||||
ConnectionInfoActiveProjectKey,
|
||||
ConnectionInfoProjectKey,
|
||||
ConnectionInfoResource,
|
||||
createConnectionParam,
|
||||
isConnectionInfoParamEqual,
|
||||
} from './ConnectionInfoResource.js';
|
||||
import { parseConnectionKey } from './parseConnectionKey.js';
|
||||
|
||||
export type ConnectionInfoProviderProperties = DatabaseConnectionProviderPropertiesFragment;
|
||||
|
||||
// TODO reuse it everywhere where include "includeProviderProperties" is used
|
||||
@injectable()
|
||||
export class ConnectionInfoProviderPropertiesResource extends CachedMapResource<IConnectionInfoParams, ConnectionInfoProviderProperties> {
|
||||
constructor(
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly graphQLService: GraphQLService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.aliases.add(ConnectionInfoProjectKey, param => resourceKeyList(this.keys.filter(key => param.options.projectIds.includes(key.projectId))));
|
||||
|
||||
this.aliases.add(ConnectionInfoActiveProjectKey, () =>
|
||||
resourceKeyList(this.keys.filter(key => projectsService.activeProjects.some(({ id }) => id === key.projectId))),
|
||||
);
|
||||
|
||||
this.sync(this.connectionInfoResource);
|
||||
this.connectionInfoResource.onItemDelete.addHandler(this.delete.bind(this));
|
||||
}
|
||||
|
||||
protected async loader(
|
||||
originalKey: ResourceKey<IConnectionInfoParams>,
|
||||
_: any,
|
||||
refresh?: boolean,
|
||||
): Promise<Map<IConnectionInfoParams, ConnectionInfoProviderProperties>> {
|
||||
const connectionsList: ConnectionInfoProviderProperties[] = [];
|
||||
let removedConnections: IConnectionInfoParams[] = [];
|
||||
const parsed = parseConnectionKey({
|
||||
originalKey,
|
||||
aliases: this.aliases,
|
||||
isOutdated: this.isOutdated.bind(this),
|
||||
activeProjects: this.projectsService.activeProjects,
|
||||
refresh,
|
||||
});
|
||||
|
||||
let { projectId } = parsed;
|
||||
const { projectIds, key } = parsed;
|
||||
|
||||
await ResourceKeyUtils.forEachAsync(key, async connectionKey => {
|
||||
let connectionId: string | undefined;
|
||||
if (!isResourceAlias(connectionKey)) {
|
||||
projectId = connectionKey.projectId;
|
||||
connectionId = connectionKey.connectionId;
|
||||
}
|
||||
|
||||
const { connections } = await this.graphQLService.sdk.getUserConnectionsProviderProperties({
|
||||
projectId,
|
||||
connectionId,
|
||||
projectIds,
|
||||
});
|
||||
|
||||
if (connectionId && !connections.some(connection => connection.id === connectionId)) {
|
||||
throw new Error(`Connection is not found (${connectionId})`);
|
||||
}
|
||||
|
||||
connectionsList.push(...connections);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (isResourceAlias(key)) {
|
||||
removedConnections = ResourceKeyUtils.toList(this.aliases.transformToKey(key)).filter(
|
||||
filterKey => !connectionsList.some(connection => isConnectionInfoParamEqual(filterKey, createConnectionParam(connection))),
|
||||
);
|
||||
}
|
||||
|
||||
this.delete(resourceKeyList(removedConnections));
|
||||
const keys = resourceKeyList(connectionsList.map(createConnectionParam));
|
||||
this.set(keys, connectionsList);
|
||||
});
|
||||
|
||||
return this.data;
|
||||
}
|
||||
|
||||
override isKeyEqual(param: IConnectionInfoParams, second: IConnectionInfoParams): boolean {
|
||||
return isConnectionInfoParamEqual(param, second);
|
||||
}
|
||||
|
||||
protected validateKey(key: IConnectionInfoParams): boolean {
|
||||
const parse = CONNECTION_INFO_PARAM_SCHEMA.safeParse(toJS(key));
|
||||
if (!parse.success) {
|
||||
this.logger.warn(`Invalid resource key ${(schemaValidationError(parse.error).toString(), { prefix: null })}`);
|
||||
}
|
||||
return parse.success;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { action, makeObservable, observable, runInAction, toJS } from 'mobx';
|
||||
|
||||
import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
@@ -270,13 +271,6 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
|
||||
return this.sessionUpdate;
|
||||
}
|
||||
|
||||
getEmptyConfig(): ConnectionConfig {
|
||||
return {
|
||||
template: false,
|
||||
saveCredentials: false,
|
||||
};
|
||||
}
|
||||
|
||||
isConnecting(key: IConnectionInfoParams): boolean;
|
||||
isConnecting(key: ResourceKeyList<IConnectionInfoParams>): boolean;
|
||||
isConnecting(key: ResourceKey<IConnectionInfoParams>): boolean;
|
||||
@@ -484,6 +478,7 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
|
||||
await this.performUpdate(key, [], async () => {
|
||||
const { connection } = await this.graphQLService.sdk.updateConnection({
|
||||
projectId: key.projectId,
|
||||
connectionId: key.connectionId,
|
||||
config,
|
||||
...this.getDefaultIncludes(),
|
||||
...this.getIncludesMap(key),
|
||||
|
||||
@@ -38,6 +38,11 @@ export * from './ConnectionInfoEventHandler.js';
|
||||
export * from './ConnectionInfoResource.js';
|
||||
export * from './ConnectionInfoOriginResource.js';
|
||||
export * from './ConnectionInfoOriginDetailsResource.js';
|
||||
export * from './ConnectionInfoAuthPropertiesResource.js';
|
||||
export * from './ConnectionInfoCustomOptionsResource.js';
|
||||
export * from './ConnectionInfoPropertiesResource.js';
|
||||
export * from './ConnectionInfoProviderPropertiesResource.js';
|
||||
export * from './ConnectionInfoNetworkHandlersResource.js';
|
||||
export * from './CONNECTIONS_SETTINGS_GROUP.js';
|
||||
export * from './EConnectionFeature.js';
|
||||
export * from './ConnectionsSettingsService.js';
|
||||
|
||||
@@ -62,4 +62,6 @@ export default [
|
||||
['connections_templates_deprecated_message', 'Template connections are deprecated and will be removed in future releases'],
|
||||
['core_connections_connection_driver_not_installed', 'Driver is not installed'],
|
||||
['core_connections_connection_temporary', 'Temporary connection'],
|
||||
['core_connections_connection_create_success', 'Connection was created'],
|
||||
['core_connections_connection_update_success', 'Connection was updated'],
|
||||
];
|
||||
|
||||
@@ -66,6 +66,8 @@ export default [
|
||||
['connections_connection_host', 'Host'],
|
||||
['connections_connection_port', 'Port'],
|
||||
['connections_connection_template', 'Template'],
|
||||
['core_connections_connection_create_success', 'Connection was created'],
|
||||
['core_connections_connection_update_success', 'Connection was updated'],
|
||||
['connections_connection_test', 'Test'],
|
||||
['connections_connection_test_tooltip', 'Test connection'],
|
||||
['connections_connection_test_fail', 'Connection test failed'],
|
||||
|
||||
@@ -76,6 +76,8 @@ export default [
|
||||
['connections_connection_port', 'Port'],
|
||||
['connections_connection_read_only', 'Read-only connection'],
|
||||
['connections_connection_template', 'Modèle'],
|
||||
['core_connections_connection_create_success', 'Connection was created'],
|
||||
['core_connections_connection_update_success', 'Connection was updated'],
|
||||
['connections_connection_test', 'Tester'],
|
||||
['connections_connection_test_tooltip', 'Tester la connexion'],
|
||||
['connections_connection_test_fail', 'Échec du test de connexion'],
|
||||
|
||||
@@ -64,6 +64,8 @@ export default [
|
||||
['connections_connection_host', 'Host'],
|
||||
['connections_connection_port', 'Porta'],
|
||||
['connections_connection_template', 'Template'],
|
||||
['core_connections_connection_create_success', 'Connection was created'],
|
||||
['core_connections_connection_update_success', 'Connection was updated'],
|
||||
['connections_connection_test', 'Prova la connessione'],
|
||||
['connections_connection_test_tooltip', 'Test connection'],
|
||||
['connections_connection_test_fail', 'Prova di connessione fallita'],
|
||||
|
||||
@@ -70,6 +70,8 @@ export default [
|
||||
['connections_connection_host', 'Хост'],
|
||||
['connections_connection_port', 'Порт'],
|
||||
['connections_connection_template', 'Шаблон'],
|
||||
['core_connections_connection_create_success', 'Подключение создано'],
|
||||
['core_connections_connection_update_success', 'Подключение обновлено'],
|
||||
['connections_connection_test', 'Тест'],
|
||||
['connections_connection_test_tooltip', 'Тестовое соединение'],
|
||||
['connections_connection_test_fail', 'Не удалось выполнить подключение'],
|
||||
|
||||
@@ -59,6 +59,8 @@ export default [
|
||||
['connections_connection_host', '主机'],
|
||||
['connections_connection_port', '端口'],
|
||||
['connections_connection_template', '模板'],
|
||||
['core_connections_connection_create_success', 'Connection was created'],
|
||||
['core_connections_connection_update_success', 'Connection was updated'],
|
||||
['connections_connection_test', '测试连接'],
|
||||
['connections_connection_test_tooltip', '测试链接'],
|
||||
['connections_connection_test_fail', '连接测试失败'],
|
||||
|
||||
@@ -20,6 +20,11 @@ export const manifest: PluginManifest = {
|
||||
() => import('./ConnectionInfoResource.js').then(m => m.ConnectionInfoResource),
|
||||
() => import('./ConnectionInfoOriginResource.js').then(m => m.ConnectionInfoOriginResource),
|
||||
() => import('./ConnectionInfoOriginDetailsResource.js').then(m => m.ConnectionInfoOriginDetailsResource),
|
||||
() => import('./ConnectionInfoAuthPropertiesResource.js').then(m => m.ConnectionInfoAuthPropertiesResource),
|
||||
() => import('./ConnectionInfoCustomOptionsResource.js').then(m => m.ConnectionInfoCustomOptionsResource),
|
||||
() => import('./ConnectionInfoPropertiesResource.js').then(m => m.ConnectionInfoPropertiesResource),
|
||||
() => import('./ConnectionInfoProviderPropertiesResource.js').then(m => m.ConnectionInfoProviderPropertiesResource),
|
||||
() => import('./ConnectionInfoNetworkHandlersResource.js').then(m => m.ConnectionInfoNetworkHandlersResource),
|
||||
() => import('./ConnectionToolsResource.js').then(m => m.ConnectionToolsResource),
|
||||
() => import('./ContainerResource.js').then(m => m.ContainerResource),
|
||||
() => import('./ConnectionsLocaleService.js').then(m => m.ConnectionsLocaleService),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
query getUserConnectionsAuthProperties($projectId: ID, $connectionId: ID, $projectIds: [ID!]) {
|
||||
connections: userConnections(projectId: $projectId, id: $connectionId, projectIds: $projectIds) {
|
||||
...DatabaseConnectionAuthProperties
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
query getUserConnectionsCustomOptions($projectId: ID, $connectionId: ID, $projectIds: [ID!]) {
|
||||
connections: userConnections(projectId: $projectId, id: $connectionId, projectIds: $projectIds) {
|
||||
...DatabaseConnectionCustomOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
query getUserConnectionsNetworkHandlers($projectId: ID, $connectionId: ID, $projectIds: [ID!]) {
|
||||
connections: userConnections(projectId: $projectId, id: $connectionId, projectIds: $projectIds) {
|
||||
...DatabaseConnectionNetworkHandlers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
query getUserConnectionsProperties($projectId: ID, $connectionId: ID, $projectIds: [ID!]) {
|
||||
connections: userConnections(projectId: $projectId, id: $connectionId, projectIds: $projectIds) {
|
||||
...DatabaseConnectionProperties
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
query getUserConnectionsProviderProperties($projectId: ID, $connectionId: ID, $projectIds: [ID!]) {
|
||||
connections: userConnections(projectId: $projectId, id: $connectionId, projectIds: $projectIds) {
|
||||
...DatabaseConnectionProviderProperties
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fragment DatabaseConnectionAuthProperties on ConnectionInfo {
|
||||
id
|
||||
projectId
|
||||
credentialsSaved
|
||||
authModel
|
||||
sharedCredentials
|
||||
requiredAuth
|
||||
authProperties {
|
||||
...UserConnectionAuthProperties
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
fragment DatabaseConnectionCustomOptions on ConnectionInfo {
|
||||
id
|
||||
projectId
|
||||
configurationType
|
||||
useUrl
|
||||
host
|
||||
port
|
||||
serverName
|
||||
databaseName
|
||||
url
|
||||
name
|
||||
description
|
||||
folder
|
||||
template
|
||||
driverId
|
||||
keepAliveInterval
|
||||
autocommit
|
||||
readOnly
|
||||
mainPropertyValues
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
fragment DatabaseConnectionNetworkHandlers on ConnectionInfo {
|
||||
id
|
||||
projectId
|
||||
networkHandlersConfig {
|
||||
...NetworkHandlerBasics
|
||||
authType
|
||||
userName
|
||||
password
|
||||
key
|
||||
savePassword
|
||||
properties
|
||||
secureProperties
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
fragment DatabaseConnectionProperties on ConnectionInfo {
|
||||
id
|
||||
projectId
|
||||
properties
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
fragment DatabaseConnectionProviderProperties on ConnectionInfo {
|
||||
id
|
||||
projectId
|
||||
providerProperties
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { isObjectsEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IFormPart } from './IFormPart.js';
|
||||
import type { IFormState } from './IFormState.js';
|
||||
import { formSubmitContext } from './formSubmitContext.js';
|
||||
|
||||
export abstract class FormPart<TPartState, TFormState = any> implements IFormPart<TPartState> {
|
||||
state: TPartState;
|
||||
@@ -39,7 +40,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
|
||||
this.loading = false;
|
||||
|
||||
this.formState.submitTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.save.bind(this)));
|
||||
this.formState.formatTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.format.bind(this)));
|
||||
this.formState.formatTask.addHandler(executorHandlerFilter(() => this.isLoaded() && this.isChanged, this.format.bind(this)));
|
||||
this.formState.validationTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.handleValidation.bind(this)));
|
||||
|
||||
makeObservable<this, 'loaded' | 'loading' | 'setInitialState'>(this, {
|
||||
@@ -84,7 +85,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
|
||||
return !isObjectsEqual(this.initialState, this.state);
|
||||
}
|
||||
|
||||
async save(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): Promise<any> {
|
||||
async save(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): Promise<void> {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
@@ -93,8 +94,9 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
|
||||
|
||||
try {
|
||||
await this.loader();
|
||||
const formSubmit = contexts.getContext(formSubmitContext);
|
||||
|
||||
if (!this.isChanged) {
|
||||
if (!this.isChanged && !formSubmit.submitOnNoChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,7 +144,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
|
||||
await this.load();
|
||||
}
|
||||
|
||||
reset() {
|
||||
reset(): void {
|
||||
this.setState(toJS(this.initialState));
|
||||
}
|
||||
|
||||
@@ -155,7 +157,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
|
||||
}
|
||||
}
|
||||
|
||||
protected setInitialState(initialState: TPartState) {
|
||||
protected setInitialState(initialState: TPartState): void {
|
||||
const isChanged = this.isChanged;
|
||||
|
||||
this.initialState = initialState;
|
||||
@@ -167,7 +169,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
|
||||
this.setState(toJS(this.initialState));
|
||||
}
|
||||
|
||||
protected setState(state: TPartState) {
|
||||
protected setState(state: TPartState): void {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { DataContext, dataContextAddDIProvider, type DataContextGetter, type IDataContext } from '@cloudbeaver/core-data-context';
|
||||
import type { IServiceProvider } from '@cloudbeaver/core-di';
|
||||
import type { ENotificationType } from '@cloudbeaver/core-events';
|
||||
import { Executor, ExecutorInterrupter, type IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor';
|
||||
import { Executor, ExecutorInterrupter, type IExecutionContext, type IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor';
|
||||
import { isArraysEqual, isNotNullDefined, MetadataMap, uuid } from '@cloudbeaver/core-utils';
|
||||
import { DATA_CONTEXT_LOADABLE_STATE, loadableStateContext } from '@cloudbeaver/core-view';
|
||||
|
||||
@@ -28,6 +28,7 @@ export class FormState<TState> implements IFormState<TState> {
|
||||
|
||||
statusMessage: string | string[] | null;
|
||||
statusType: ENotificationType | null;
|
||||
isReadOnly: boolean;
|
||||
|
||||
savingPromise: Promise<any> | null;
|
||||
|
||||
@@ -61,14 +62,14 @@ export class FormState<TState> implements IFormState<TState> {
|
||||
|
||||
this.statusMessage = null;
|
||||
this.statusType = null;
|
||||
|
||||
this.isReadOnly = false;
|
||||
this.savingPromise = null;
|
||||
|
||||
this.formStateTask = new Executor<TState>(state, () => true);
|
||||
this.formStateTask.addCollection(service.onState).addPostHandler(this.updateFormState.bind(this));
|
||||
|
||||
this.loadedTask = new Executor(this as IFormState<TState>, () => true);
|
||||
this.loadedTask.addCollection(service.onLoaded).next(this.formStateTask).addPostHandler(this.onLoadedHandler.bind(this));
|
||||
this.loadedTask.addCollection(service.onLoaded).next(this.formStateTask);
|
||||
|
||||
this.formatTask = new Executor(this as IFormState<TState>, () => true);
|
||||
this.formatTask.addCollection(service.onFormat);
|
||||
@@ -106,7 +107,7 @@ export class FormState<TState> implements IFormState<TState> {
|
||||
});
|
||||
}
|
||||
|
||||
get partsValues() {
|
||||
get partsValues(): IFormPart<any>[] {
|
||||
return Array.from(this.parts.values());
|
||||
}
|
||||
|
||||
@@ -172,9 +173,9 @@ export class FormState<TState> implements IFormState<TState> {
|
||||
return this;
|
||||
}
|
||||
|
||||
async save(): Promise<boolean> {
|
||||
async save(providedContext?: IExecutionContext<IFormState<TState>>): Promise<boolean> {
|
||||
try {
|
||||
this.savingPromise = this.submitTask.execute(this);
|
||||
this.savingPromise = this.submitTask.execute(this, providedContext);
|
||||
const context = await this.savingPromise;
|
||||
|
||||
if (ExecutorInterrupter.isInterrupted(context)) {
|
||||
@@ -190,15 +191,6 @@ export class FormState<TState> implements IFormState<TState> {
|
||||
return false;
|
||||
}
|
||||
|
||||
private onLoadedHandler(data: IFormState<TState>, contexts: IExecutionContextProvider<IFormState<TState>>): void {
|
||||
for (const part of this.parts.values()) {
|
||||
if (!part.isLoaded()) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateFormState(data: TState, contexts: IExecutionContextProvider<TState>): void {
|
||||
const context = contexts.getContext(formStateContext);
|
||||
|
||||
@@ -208,6 +200,7 @@ export class FormState<TState> implements IFormState<TState> {
|
||||
|
||||
this.statusMessage = context.statusMessage;
|
||||
this.statusType = context.statusType;
|
||||
this.isReadOnly = context.readonly;
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface IFormPart<TState> extends ILoadableState {
|
||||
readonly isChanged: boolean;
|
||||
|
||||
load(): Promise<void>;
|
||||
reload(): Promise<void>;
|
||||
reset(): void;
|
||||
dispose(): void | Promise<void>;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import type { DataContextGetter, IDataContext } from '@cloudbeaver/core-data-context';
|
||||
import type { ENotificationType } from '@cloudbeaver/core-events';
|
||||
import type { IExecutor } from '@cloudbeaver/core-executor';
|
||||
import type { IExecutionContext, IExecutor } from '@cloudbeaver/core-executor';
|
||||
import type { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { FormBaseService } from './FormBaseService.js';
|
||||
@@ -45,8 +45,9 @@ export interface IFormState<TState> {
|
||||
isError: boolean;
|
||||
isCancelled: boolean;
|
||||
isChanged: boolean;
|
||||
isReadOnly: boolean;
|
||||
|
||||
save(): Promise<boolean>;
|
||||
save(providedContext?: IExecutionContext<IFormState<TState>>): Promise<boolean>;
|
||||
reset(): void;
|
||||
cancel(): void;
|
||||
dispose(): void | Promise<void>;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export interface IFormSubmitContext {
|
||||
type: string;
|
||||
submitOnNoChanges: boolean;
|
||||
setType(type: string): void;
|
||||
setSubmitOnNoChanges(submitOnNoChanges: boolean): void;
|
||||
}
|
||||
|
||||
export function formSubmitContext(): IFormSubmitContext {
|
||||
return {
|
||||
type: 'submit',
|
||||
submitOnNoChanges: false,
|
||||
setType(type) {
|
||||
this.type = type;
|
||||
},
|
||||
setSubmitOnNoChanges(submitOnNoChanges) {
|
||||
this.submitOnNoChanges = submitOnNoChanges;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { Suspense, useContext, useMemo } from 'react';
|
||||
|
||||
import { Translate } from '@cloudbeaver/core-blocks';
|
||||
|
||||
@@ -47,15 +47,18 @@ export function TabDefault<T = Record<string, any>>({
|
||||
const TabComponent = component;
|
||||
return (
|
||||
<TabContext.Provider value={tabContext}>
|
||||
<TabComponent
|
||||
tabId={tabId}
|
||||
className={className}
|
||||
{...(rest as unknown as T)}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
{/* TODO: do we want to fallback to default tab with provided name and icon? */}
|
||||
<TabComponent
|
||||
tabId={tabId}
|
||||
className={className}
|
||||
{...(rest as unknown as T)}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</Suspense>
|
||||
</TabContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -33,6 +33,7 @@ export * from './Form/FormPart.js';
|
||||
export * from './Form/formStateContext.js';
|
||||
export * from './Form/formStatusContext.js';
|
||||
export * from './Form/formValidationContext.js';
|
||||
export * from './Form/formSubmitContext.js';
|
||||
export * from './Form/IFormPart.js';
|
||||
export * from './Form/IFormProps.js';
|
||||
export * from './Form/IFormState.js';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 function getSubjectDifferences(current: string[], next: string[]): { subjectsToRevoke: string[]; subjectsToGrant: string[] } {
|
||||
const subjectsToRevoke = current.filter(subjectId => !next.includes(subjectId));
|
||||
const subjectsToGrant = next.filter(subjectId => !current.includes(subjectId));
|
||||
|
||||
return { subjectsToRevoke, subjectsToGrant };
|
||||
}
|
||||
@@ -88,3 +88,4 @@ export * from './types/UndefinedToNull.js';
|
||||
export * from './bindFunctions.js';
|
||||
export * from './getDomainFromUrl.js';
|
||||
export * from './isNumber.js';
|
||||
export * from './getSubjectDifferences.js';
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import pnpapi from 'pnpapi';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { makeObservable, observable } from 'mobx';
|
||||
|
||||
import { ConfirmationDialog } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
ConnectionInfoOriginResource,
|
||||
ConnectionInfoResource,
|
||||
ConnectionsManagerService,
|
||||
createConnectionParam,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
|
||||
import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoResource, ConnectionsManagerService, createConnectionParam } from '@cloudbeaver/core-connections';
|
||||
import { injectable, IServiceProvider } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { ExecutorInterrupter, type IExecutorHandler } from '@cloudbeaver/core-executor';
|
||||
import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
|
||||
import { OptionsPanelService } from '@cloudbeaver/core-ui';
|
||||
import { ConnectionFormService, ConnectionFormState, type IConnectionFormState } from '@cloudbeaver/plugin-connections';
|
||||
import { ConnectionFormService, ConnectionFormState, getConnectionFormOptionsPart } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
import { SearchDatabase } from './SearchDatabase.js';
|
||||
const SearchDatabase = importLazyComponent(() => import('./SearchDatabase.js').then(module => module.SearchDatabase));
|
||||
|
||||
const formGetter = () => SearchDatabase;
|
||||
|
||||
@@ -34,18 +29,16 @@ export class ConnectionSearchService {
|
||||
|
||||
disabled = false;
|
||||
|
||||
formState: IConnectionFormState | null = null;
|
||||
formState: ConnectionFormState | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly serviceProvider: IServiceProvider,
|
||||
private readonly optionsPanelService: OptionsPanelService,
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly projectInfoResource: ProjectInfoResource,
|
||||
private readonly connectionsManagerService: ConnectionsManagerService,
|
||||
private readonly connectionInfoOriginResource: ConnectionInfoOriginResource,
|
||||
) {
|
||||
this.optionsPanelService.closeTask.addHandler(this.closeHandler);
|
||||
|
||||
@@ -59,6 +52,7 @@ export class ConnectionSearchService {
|
||||
databases: observable,
|
||||
disabled: observable,
|
||||
formState: observable.shallow,
|
||||
select: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,20 +104,22 @@ export class ConnectionSearchService {
|
||||
this.close();
|
||||
};
|
||||
|
||||
private get optionsPart() {
|
||||
return this.formState ? getConnectionFormOptionsPart(this.formState) : null;
|
||||
}
|
||||
|
||||
private async showUnsavedChangesDialog(): Promise<boolean> {
|
||||
if (
|
||||
!this.formState ||
|
||||
!this.optionsPanelService.isOpen(formGetter) ||
|
||||
(this.formState.config.connectionId &&
|
||||
this.formState.projectId !== null &&
|
||||
!this.connectionInfoResource.has(createConnectionParam(this.formState.projectId, this.formState.config.connectionId)))
|
||||
(this.optionsPart?.state.connectionId &&
|
||||
this.formState.state.projectId !== null &&
|
||||
!this.connectionInfoResource.has(createConnectionParam(this.formState.state.projectId, this.optionsPart.state.connectionId)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const state = await this.formState.checkFormState();
|
||||
|
||||
if (!state?.edited) {
|
||||
if (!this.formState.isChanged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,15 +136,15 @@ export class ConnectionSearchService {
|
||||
this.hosts = hosts;
|
||||
}
|
||||
|
||||
saveConnection() {
|
||||
saveConnection(): void {
|
||||
this.goBack();
|
||||
}
|
||||
|
||||
goBack() {
|
||||
goBack(): void {
|
||||
this.clearFormState();
|
||||
}
|
||||
|
||||
select(database: AdminConnectionSearchInfo): void {
|
||||
async select(database: AdminConnectionSearchInfo): Promise<void> {
|
||||
const projects = this.connectionsManagerService.createConnectionProjects;
|
||||
|
||||
if (projects.length === 0) {
|
||||
@@ -156,29 +152,23 @@ export class ConnectionSearchService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.formState) {
|
||||
this.formState = new ConnectionFormState(
|
||||
this.projectsService,
|
||||
this.projectInfoResource,
|
||||
this.connectionFormService,
|
||||
this.connectionInfoResource,
|
||||
this.connectionInfoOriginResource,
|
||||
);
|
||||
this.formState?.dispose();
|
||||
this.formState = new ConnectionFormState(this.serviceProvider, this.connectionFormService, {
|
||||
projectId: projects[0]!.id,
|
||||
availableDrivers: database.possibleDrivers,
|
||||
type: 'public',
|
||||
requiredNetworkHandlersIds: [],
|
||||
connectionId: undefined,
|
||||
});
|
||||
|
||||
this.formState.closeTask.addHandler(this.goBack.bind(this));
|
||||
}
|
||||
await this.optionsPart?.load();
|
||||
await this.optionsPart?.setDriverId(database.defaultDriver);
|
||||
|
||||
this.formState
|
||||
.setOptions('create', 'public')
|
||||
.setConfig(projects[0]!.id, {
|
||||
...this.connectionInfoResource.getEmptyConfig(),
|
||||
driverId: database.defaultDriver,
|
||||
host: database.host,
|
||||
port: `${database.port}`,
|
||||
})
|
||||
.setAvailableDrivers(database.possibleDrivers);
|
||||
this.optionsPart!.state.host = database.host;
|
||||
this.optionsPart!.state.port = String(database.port);
|
||||
this.optionsPart!.state.driverId = database.defaultDriver;
|
||||
|
||||
this.formState.load();
|
||||
this.formState.disposeTask.addHandler(this.goBack.bind(this));
|
||||
}
|
||||
|
||||
private clearFormState() {
|
||||
|
||||
@@ -34,7 +34,7 @@ export const SearchDatabase: React.FC = observer(function SearchDatabase() {
|
||||
return (
|
||||
<Loader className={s(styles, { loader: true })} suspense>
|
||||
<ConnectionFormLoader
|
||||
state={connectionSearchService.formState}
|
||||
formState={connectionSearchService.formState}
|
||||
onSave={() => connectionSearchService.saveConnection()}
|
||||
onCancel={() => connectionSearchService.goBack()}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@cloudbeaver/core-authentication": "workspace:*",
|
||||
"@cloudbeaver/core-blocks": "workspace:*",
|
||||
"@cloudbeaver/core-connections": "workspace:*",
|
||||
"@cloudbeaver/core-data-context": "workspace:*",
|
||||
"@cloudbeaver/core-di": "workspace:*",
|
||||
"@cloudbeaver/core-dialogs": "workspace:*",
|
||||
"@cloudbeaver/core-events": "workspace:*",
|
||||
|
||||
+14
-21
@@ -1,48 +1,41 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 { useMemo } from 'react';
|
||||
|
||||
import { Loader, s, useS } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoOriginResource, ConnectionInfoResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { type IConnectionInfoParams } from '@cloudbeaver/core-connections';
|
||||
import { ConnectionFormLoader, useConnectionFormState } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
import styles from './ConnectionEdit.module.css';
|
||||
import { FormMode } from '@cloudbeaver/core-ui';
|
||||
import { runInAction } from 'mobx';
|
||||
|
||||
interface Props {
|
||||
item: IConnectionInfoParams;
|
||||
}
|
||||
|
||||
export const ConnectionEdit = observer<Props>(function ConnectionEditNew({ item }) {
|
||||
const connectionInfoResource = useService(ConnectionInfoResource);
|
||||
const connectionInfoOriginResource = useService(ConnectionInfoOriginResource);
|
||||
// const tableContext = useContext(TableContext);
|
||||
// const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext, item]);
|
||||
const data = useConnectionFormState(item, state => {
|
||||
state.setMode(FormMode.Edit);
|
||||
|
||||
const data = useConnectionFormState(connectionInfoResource, connectionInfoOriginResource, state => state.setOptions('edit', 'admin'));
|
||||
runInAction(() => {
|
||||
state.state.connectionId = item.connectionId;
|
||||
state.state.projectId = item.projectId;
|
||||
state.state.type = 'admin';
|
||||
});
|
||||
});
|
||||
const style = useS(styles);
|
||||
|
||||
const projectId = item.projectId;
|
||||
const connectionId = item.connectionId;
|
||||
|
||||
useMemo(() => {
|
||||
data.setConfig(projectId, { connectionId });
|
||||
}, [data, projectId, connectionId]);
|
||||
|
||||
return (
|
||||
<div className={s(style, { box: true })}>
|
||||
<Loader className={s(style, { loader: true })} suspense>
|
||||
<ConnectionFormLoader
|
||||
state={data}
|
||||
// onCancel={collapse}
|
||||
// onSave={collapse}
|
||||
/>
|
||||
<ConnectionFormLoader formState={data} />
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
|
||||
+6
-4
@@ -1,10 +1,11 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 {
|
||||
@@ -25,7 +26,7 @@ import {
|
||||
import { DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { TabListStyles, TabPanelList, TabPanelStyles, TabsState, TabStyles } from '@cloudbeaver/core-ui';
|
||||
import { ConnectionFormLoader } from '@cloudbeaver/plugin-connections';
|
||||
import { ConnectionFormLoader, getConnectionFormOptionsPart } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
import { CreateConnectionService } from '../CreateConnectionService.js';
|
||||
import styles from './shared/CreateConnection.module.css';
|
||||
@@ -48,7 +49,8 @@ export const CreateConnection = observer<Props>(function CreateConnection({ meth
|
||||
const style = useS(styles);
|
||||
const createConnectionService = useService(CreateConnectionService);
|
||||
const translate = useTranslate();
|
||||
const driver = useResource(CreateConnection, DBDriverResource, createConnectionService.data?.config.driverId || null);
|
||||
const optionsPart = createConnectionService.data ? getConnectionFormOptionsPart(createConnectionService.data) : null;
|
||||
const driver = useResource(CreateConnection, DBDriverResource, optionsPart?.state.driverId || null);
|
||||
|
||||
if (createConnectionService.data) {
|
||||
return (
|
||||
@@ -75,7 +77,7 @@ export const CreateConnection = observer<Props>(function CreateConnection({ meth
|
||||
<Container overflow>
|
||||
<Loader className={s(style, { loader: true })} suspense>
|
||||
<ConnectionFormLoader
|
||||
state={createConnectionService.data}
|
||||
formState={createConnectionService.data}
|
||||
onCancel={createConnectionService.clearConnectionTemplate}
|
||||
onSave={createConnectionService.clearConnectionTemplate}
|
||||
/>
|
||||
|
||||
+3
-4
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 { ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
|
||||
import { CreateConnectionService } from '../../CreateConnectionService.js';
|
||||
@@ -20,7 +20,7 @@ export class ConnectionManualService {
|
||||
this.createConnectionService.disabled = value;
|
||||
}
|
||||
|
||||
constructor(private readonly connectionInfoResource: ConnectionInfoResource, private readonly createConnectionService: CreateConnectionService) {
|
||||
constructor(private readonly createConnectionService: CreateConnectionService) {
|
||||
this.select = this.select.bind(this);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export class ConnectionManualService {
|
||||
this.createConnectionService.setConnectionTemplate(
|
||||
projectId,
|
||||
{
|
||||
...this.connectionInfoResource.getEmptyConfig(),
|
||||
template: true,
|
||||
driverId,
|
||||
},
|
||||
|
||||
+22
-24
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -8,12 +8,10 @@
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
|
||||
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import { ConnectionInfoOriginResource, ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { injectable, IServiceProvider } from '@cloudbeaver/core-di';
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
import { TabsContainer } from '@cloudbeaver/core-ui';
|
||||
import { ConnectionFormService, ConnectionFormState, type IConnectionFormState } from '@cloudbeaver/plugin-connections';
|
||||
import { ConnectionFormService, ConnectionFormState, getConnectionFormOptionsPart } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
import { ConnectionsAdministrationNavService } from './ConnectionsAdministrationNavService.js';
|
||||
|
||||
@@ -27,18 +25,15 @@ export interface ICreateMethodOptions {
|
||||
@injectable()
|
||||
export class CreateConnectionService {
|
||||
disabled = false;
|
||||
data: IConnectionFormState | null;
|
||||
data: ConnectionFormState | null;
|
||||
|
||||
readonly tabsContainer: TabsContainer<void, ICreateMethodOptions>;
|
||||
|
||||
constructor(
|
||||
private readonly connectionsAdministrationNavService: ConnectionsAdministrationNavService,
|
||||
private readonly administrationScreenService: AdministrationScreenService,
|
||||
private readonly serviceProvider: IServiceProvider,
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly projectInfoResource: ProjectInfoResource,
|
||||
private readonly connectionInfoOriginResource: ConnectionInfoOriginResource,
|
||||
) {
|
||||
this.data = null;
|
||||
this.tabsContainer = new TabsContainer('Connection Creation mode');
|
||||
@@ -109,23 +104,26 @@ export class CreateConnectionService {
|
||||
this.activateMethod(defaultId);
|
||||
}
|
||||
|
||||
setConnectionTemplate(projectId: string, config: ConnectionConfig, availableDrivers: string[]): void {
|
||||
this.data = new ConnectionFormState(
|
||||
this.projectsService,
|
||||
this.projectInfoResource,
|
||||
this.connectionFormService,
|
||||
this.connectionInfoResource,
|
||||
this.connectionInfoOriginResource,
|
||||
);
|
||||
private get optionsPart() {
|
||||
return this.data ? getConnectionFormOptionsPart(this.data) : null;
|
||||
}
|
||||
|
||||
this.data.closeTask.addHandler(this.cancelCreate.bind(this));
|
||||
async setConnectionTemplate(projectId: string, config: ConnectionConfig, availableDrivers: string[]): Promise<void> {
|
||||
this.clearConnectionTemplate();
|
||||
this.data = new ConnectionFormState(this.serviceProvider, this.connectionFormService, {
|
||||
projectId,
|
||||
availableDrivers,
|
||||
type: 'admin',
|
||||
requiredNetworkHandlersIds: [],
|
||||
connectionId: config.connectionId,
|
||||
});
|
||||
|
||||
this.data
|
||||
.setOptions('create', 'admin')
|
||||
.setConfig(projectId, config)
|
||||
.setAvailableDrivers(availableDrivers || []);
|
||||
await this.optionsPart?.load();
|
||||
await this.optionsPart?.setDriverId(config.driverId);
|
||||
|
||||
this.data.load();
|
||||
Object.assign(this.optionsPart!.state, config);
|
||||
|
||||
this.data.disposeTask.addHandler(this.cancelCreate.bind(this));
|
||||
}
|
||||
|
||||
clearConnectionTemplate(): void {
|
||||
|
||||
+63
-29
@@ -1,13 +1,14 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { computed } from 'mobx';
|
||||
|
||||
import { action, computed, observable } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { TeamsResource, UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication';
|
||||
import {
|
||||
@@ -19,29 +20,43 @@ import {
|
||||
s,
|
||||
TextPlaceholder,
|
||||
useAutoLoad,
|
||||
useObservableRef,
|
||||
useResource,
|
||||
useS,
|
||||
useTranslate,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { isCloudConnection } from '@cloudbeaver/core-connections';
|
||||
import { ConnectionInfoOriginResource, ConnectionInfoResource, createConnectionParam, isCloudConnection } from '@cloudbeaver/core-connections';
|
||||
import type { TLocalizationToken } from '@cloudbeaver/core-localization';
|
||||
import { CachedMapAllKey, CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource';
|
||||
import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui';
|
||||
import type { IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
import { FormMode, type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui';
|
||||
import { getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
import styles from './ConnectionAccess.module.css';
|
||||
import { ConnectionAccessGrantedList } from './ConnectionAccessGrantedList.js';
|
||||
import { ConnectionAccessList } from './ConnectionAccessList.js';
|
||||
import { useConnectionAccessState } from './useConnectionAccessState.js';
|
||||
import { getConnectionFormAccessPart } from './getConnectionFormAccessPart.js';
|
||||
|
||||
export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps> = observer(function ConnectionAccess({ tabId, state: formState }) {
|
||||
const state = useConnectionAccessState(formState.info);
|
||||
export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps> = observer(function ConnectionAccess({ tabId, formState }) {
|
||||
const translate = useTranslate();
|
||||
const style = useS(styles);
|
||||
const state = useObservableRef(
|
||||
() => ({
|
||||
editing: false,
|
||||
toggleEdit() {
|
||||
this.editing = !this.editing;
|
||||
},
|
||||
}),
|
||||
{
|
||||
editing: observable.ref,
|
||||
toggleEdit: action.bound,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const { selected } = useTab(tabId);
|
||||
const accessPart = getConnectionFormAccessPart(formState);
|
||||
const [initialFormMode] = useState<FormMode>(formState.mode);
|
||||
|
||||
useAutoLoad(ConnectionAccess, state, selected);
|
||||
useAutoLoad(ConnectionAccess, accessPart, selected);
|
||||
|
||||
const users = useResource(ConnectionAccess, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setParent(UsersResourceFilterKey()), {
|
||||
active: selected,
|
||||
@@ -49,32 +64,51 @@ export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps>
|
||||
const teams = useResource(ConnectionAccess, TeamsResource, CachedMapAllKey, { active: selected });
|
||||
|
||||
const grantedUsers = useMemo(
|
||||
() => computed(() => users.resource.values.filter(user => state.state.grantedSubjects.includes(user.userId))),
|
||||
[state.state.grantedSubjects, users.resource],
|
||||
() => computed(() => users.resource.values.filter(user => accessPart.state.includes(user.userId))),
|
||||
[accessPart.state, users.resource],
|
||||
);
|
||||
|
||||
const grantedTeams = useMemo(
|
||||
() => computed(() => teams.resource.values.filter(team => state.state.grantedSubjects.includes(team.teamId))),
|
||||
[state.state.grantedSubjects, teams.resource],
|
||||
() => computed(() => teams.resource.values.filter(team => accessPart.state.includes(team.teamId))),
|
||||
[accessPart.state, teams.resource],
|
||||
);
|
||||
|
||||
if (!selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const loading = users.isLoading() || teams.isLoading() || state.state.loading;
|
||||
const cloud = formState.info && formState.originInfo?.origin ? isCloudConnection(formState.originInfo.origin) : false;
|
||||
const disabled = loading || !state.state.loaded || formState.disabled || cloud;
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const connectionInfoResource = useResource(
|
||||
ConnectionAccess,
|
||||
ConnectionInfoResource,
|
||||
createConnectionParam(formState.state.projectId, optionsPart.state.connectionId!),
|
||||
{
|
||||
active: selected && !!formState.state.projectId && !!optionsPart.state.connectionId,
|
||||
},
|
||||
);
|
||||
const originInfoResource = useResource(
|
||||
ConnectionAccess,
|
||||
ConnectionInfoOriginResource,
|
||||
createConnectionParam(formState.state.projectId, optionsPart.state.connectionId!),
|
||||
{
|
||||
active: selected && !!formState.state.projectId && !!optionsPart.state.connectionId,
|
||||
},
|
||||
);
|
||||
const connectionInfo = connectionInfoResource.data;
|
||||
const originInfo = originInfoResource.data;
|
||||
const loading = users.isLoading() || teams.isLoading() || accessPart.isLoading();
|
||||
const cloud = connectionInfo && originInfo?.origin ? isCloudConnection(originInfo.origin) : false;
|
||||
const disabled = loading || !accessPart.isLoaded() || formState.isDisabled || cloud;
|
||||
let info: TLocalizationToken | null = null;
|
||||
|
||||
if (formState.mode === 'edit' && state.changed) {
|
||||
if (initialFormMode === FormMode.Edit && accessPart.isChanged) {
|
||||
info = 'ui_save_reminder';
|
||||
} else if (cloud) {
|
||||
info = 'cloud_connections_access_placeholder';
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Loader className={s(style, { loader: true })} state={[users, teams, state.state]}>
|
||||
<Loader className={s(style, { loader: true })} state={[users, teams, accessPart]}>
|
||||
{() => (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent gap vertical>
|
||||
{!users.resource.values.length && !teams.resource.values.length ? (
|
||||
@@ -89,16 +123,16 @@ export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps>
|
||||
grantedUsers={grantedUsers.get()}
|
||||
grantedTeams={grantedTeams.get()}
|
||||
disabled={disabled}
|
||||
onEdit={state.edit}
|
||||
onRevoke={state.revoke}
|
||||
onEdit={state.toggleEdit}
|
||||
onRevoke={accessPart.revoke}
|
||||
/>
|
||||
{state.state.editing && (
|
||||
{state.editing && (
|
||||
<ConnectionAccessList
|
||||
userList={users.resource.values}
|
||||
teamList={teams.resource.values}
|
||||
grantedSubjects={state.state.grantedSubjects}
|
||||
grantedSubjects={accessPart.state}
|
||||
disabled={disabled}
|
||||
onGrant={state.grant}
|
||||
onGrant={accessPart.grant}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
+21
-116
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -8,22 +8,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import { ConnectionInfoResource, createConnectionParam, type IConnectionInfoParams } from '@cloudbeaver/core-connections';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { executorHandlerFilter, type IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { EAdminPermission, PermissionsService } from '@cloudbeaver/core-root';
|
||||
import { formStateContext } from '@cloudbeaver/core-ui';
|
||||
import type { MetadataValueGetter } from '@cloudbeaver/core-utils';
|
||||
import {
|
||||
connectionConfigContext,
|
||||
ConnectionFormService,
|
||||
type IConnectionFormProps,
|
||||
type IConnectionFormState,
|
||||
type IConnectionFormSubmitData,
|
||||
} from '@cloudbeaver/plugin-connections';
|
||||
|
||||
import type { IConnectionAccessTabState } from './IConnectionAccessTabState.js';
|
||||
import { EAdminPermission, PermissionsService, SessionPermissionsResource } from '@cloudbeaver/core-root';
|
||||
import { ConnectionFormService, getConnectionFormOptionsPart } from '@cloudbeaver/plugin-connections';
|
||||
import { getConnectionFormAccessPart } from './getConnectionFormAccessPart.js';
|
||||
import { getCachedDataResourceLoaderState, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource';
|
||||
|
||||
const ConnectionAccess = React.lazy(async () => {
|
||||
const { ConnectionAccess } = await import('./ConnectionAccess.js');
|
||||
@@ -35,9 +25,9 @@ export class ConnectionAccessTabService extends Bootstrap {
|
||||
private readonly key: string;
|
||||
|
||||
constructor(
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly ConnectionFormService: ConnectionFormService,
|
||||
private readonly administrationScreenService: AdministrationScreenService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
private readonly sessionPermissionsResource: SessionPermissionsResource,
|
||||
private readonly permissionsResource: PermissionsService,
|
||||
private readonly projectInfoResource: ProjectInfoResource,
|
||||
) {
|
||||
@@ -46,112 +36,27 @@ export class ConnectionAccessTabService extends Bootstrap {
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.tabsContainer.add({
|
||||
this.ConnectionFormService.parts.add({
|
||||
key: this.key,
|
||||
name: 'connections_connection_edit_access',
|
||||
title: 'connections_connection_edit_access',
|
||||
order: 4,
|
||||
stateGetter: context => this.stateGetter(context),
|
||||
isHidden: (_, context) => !context || !this.isAccessTabActive(context.state),
|
||||
isDisabled: (tabId, props) => !props?.state.config.driverId || this.administrationScreenService.isConfigurationMode,
|
||||
stateGetter: context => () => getConnectionFormAccessPart(context.formState),
|
||||
getLoader: (_, context) => [
|
||||
getCachedMapResourceLoaderState(this.projectInfoResource, () => context?.formState.state.projectId ?? null),
|
||||
getCachedDataResourceLoaderState(this.sessionPermissionsResource, () => undefined),
|
||||
],
|
||||
isHidden: (_, context) => !context || !this.isAccessTabActive(context.formState.state.projectId),
|
||||
isDisabled: (tabId, props) => {
|
||||
const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null;
|
||||
|
||||
return !optionsPart?.state.driverId || this.administrationScreenService.isConfigurationMode;
|
||||
},
|
||||
panel: () => ConnectionAccess,
|
||||
});
|
||||
|
||||
this.connectionFormService.formSubmittingTask.addHandler(executorHandlerFilter(data => this.isAccessTabActive(data.state), this.save.bind(this)));
|
||||
|
||||
this.connectionFormService.formStateTask.addHandler(executorHandlerFilter(this.isAccessTabActive.bind(this), this.formState.bind(this)));
|
||||
}
|
||||
|
||||
private isAccessTabActive(state: IConnectionFormState): boolean {
|
||||
return (
|
||||
state.projectId !== null &&
|
||||
isGlobalProject(this.projectInfoResource.get(state.projectId)) &&
|
||||
this.permissionsResource.has(EAdminPermission.admin)
|
||||
);
|
||||
}
|
||||
|
||||
private stateGetter(context: IConnectionFormProps): MetadataValueGetter<string, IConnectionAccessTabState> {
|
||||
return () => ({
|
||||
loading: false,
|
||||
loaded: false,
|
||||
editing: false,
|
||||
grantedSubjects: [],
|
||||
initialGrantedSubjects: [],
|
||||
});
|
||||
}
|
||||
|
||||
private async save(data: IConnectionFormSubmitData, contexts: IExecutionContextProvider<IConnectionFormSubmitData>) {
|
||||
if (data.submitType === 'test' || !data.state.projectId) {
|
||||
return;
|
||||
}
|
||||
const status = contexts.getContext(this.connectionFormService.connectionStatusContext);
|
||||
|
||||
if (!status.saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
const state = this.connectionFormService.tabsContainer.getTabState<IConnectionAccessTabState>(data.state.partsState, this.key, {
|
||||
state: data.state,
|
||||
});
|
||||
|
||||
if (!config.connectionId || !state.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = createConnectionParam(data.state.projectId, config.connectionId);
|
||||
|
||||
const currentGrantedSubjects = await this.connectionInfoResource.loadAccessSubjects(key);
|
||||
const currentGrantedSubjectIds = currentGrantedSubjects.map(subject => subject.subjectId);
|
||||
|
||||
const { subjectsToRevoke, subjectsToGrant } = this.getSubjectDifferences(currentGrantedSubjectIds, state.grantedSubjects);
|
||||
|
||||
if (subjectsToRevoke.length === 0 && subjectsToGrant.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subjectsToRevoke.length > 0) {
|
||||
await this.connectionInfoResource.deleteConnectionsAccess(key, subjectsToRevoke);
|
||||
}
|
||||
|
||||
if (subjectsToGrant.length > 0) {
|
||||
await this.connectionInfoResource.addConnectionsAccess(key, subjectsToGrant);
|
||||
}
|
||||
|
||||
state.initialGrantedSubjects = state.grantedSubjects.slice();
|
||||
}
|
||||
|
||||
private async formState(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
const state = this.connectionFormService.tabsContainer.getTabState<IConnectionAccessTabState>(data.partsState, this.key, { state: data });
|
||||
|
||||
if (!config.connectionId || !data.projectId || !state.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = createConnectionParam(data.projectId, config.connectionId);
|
||||
const changed = await this.isChanged(key, state.grantedSubjects);
|
||||
|
||||
if (changed) {
|
||||
const stateContext = contexts.getContext(formStateContext);
|
||||
|
||||
stateContext.markEdited();
|
||||
}
|
||||
}
|
||||
|
||||
private async isChanged(connectionKey: IConnectionInfoParams, next: string[]): Promise<boolean> {
|
||||
const current = await this.connectionInfoResource.loadAccessSubjects(connectionKey);
|
||||
if (current.length !== next.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return current.some(value => !next.some(subjectId => subjectId === value.subjectId));
|
||||
}
|
||||
|
||||
private getSubjectDifferences(current: string[], next: string[]): { subjectsToRevoke: string[]; subjectsToGrant: string[] } {
|
||||
const subjectsToRevoke = current.filter(subjectId => !next.includes(subjectId));
|
||||
const subjectsToGrant = next.filter(subjectId => !current.includes(subjectId));
|
||||
|
||||
return { subjectsToRevoke, subjectsToGrant };
|
||||
private isAccessTabActive(projectId: string | null): boolean {
|
||||
return projectId !== null && isGlobalProject(this.projectInfoResource.get(projectId)) && this.permissionsResource.has(EAdminPermission.admin);
|
||||
}
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { FormPart, formSubmitContext, type IFormState } from '@cloudbeaver/core-ui';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { type ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { action, makeObservable } from 'mobx';
|
||||
import { type IConnectionFormState, ConnectionFormOptionsPart } from '@cloudbeaver/plugin-connections';
|
||||
import { getSubjectDifferences } from '@cloudbeaver/core-utils';
|
||||
|
||||
function getDefaultState(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export class ConnectionFormAccessPart extends FormPart<string[], IConnectionFormState> {
|
||||
constructor(
|
||||
formState: IFormState<IConnectionFormState>,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
private readonly optionsPart: ConnectionFormOptionsPart,
|
||||
) {
|
||||
super(formState, getDefaultState());
|
||||
|
||||
makeObservable(this, {
|
||||
revoke: action.bound,
|
||||
grant: action.bound,
|
||||
});
|
||||
}
|
||||
|
||||
override isOutdated(): boolean {
|
||||
return Boolean(this.optionsPart.connectionKey && this.connectionInfoResource.isOutdated(this.optionsPart.connectionKey));
|
||||
}
|
||||
|
||||
protected override async loader(): Promise<void> {
|
||||
if (!this.optionsPart.connectionKey) {
|
||||
this.setInitialState(getDefaultState());
|
||||
return;
|
||||
}
|
||||
|
||||
await this.connectionInfoResource.load(this.optionsPart.connectionKey);
|
||||
const subjects = await this.connectionInfoResource.loadAccessSubjects(this.optionsPart.connectionKey);
|
||||
|
||||
this.setInitialState(subjects.map(subject => subject.subjectId));
|
||||
}
|
||||
|
||||
protected override async saveChanges(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {
|
||||
const submitInfo = contexts.getContext(formSubmitContext);
|
||||
if (submitInfo.type === 'test' || !this.optionsPart.connectionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { subjectsToRevoke, subjectsToGrant } = getSubjectDifferences(this.initialState, this.state);
|
||||
|
||||
const promises = [];
|
||||
|
||||
if (subjectsToRevoke.length > 0) {
|
||||
promises.push(this.connectionInfoResource.deleteConnectionsAccess(this.optionsPart.connectionKey, subjectsToRevoke));
|
||||
}
|
||||
|
||||
if (subjectsToGrant.length > 0) {
|
||||
promises.push(this.connectionInfoResource.addConnectionsAccess(this.optionsPart.connectionKey, subjectsToGrant));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
revoke(subjectIds: string[]) {
|
||||
this.state = this.state.filter(subject => !subjectIds.includes(subject));
|
||||
}
|
||||
|
||||
grant(subjectIds: string[]) {
|
||||
this.state.push(...subjectIds);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context';
|
||||
import type { IFormState } from '@cloudbeaver/core-ui';
|
||||
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { ConnectionFormAccessPart } from './ConnectionFormAccessPart.js';
|
||||
import { getConnectionFormOptionsPart, type IConnectionFormState } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
const DATA_CONTEXT_CONNECTION_FORM_ACCESS_PART = createDataContext<ConnectionFormAccessPart>('Connection Form Access Part');
|
||||
|
||||
export function getConnectionFormAccessPart(formState: IFormState<IConnectionFormState>): ConnectionFormAccessPart {
|
||||
return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_ACCESS_PART, context => {
|
||||
const di = context.get(DATA_CONTEXT_DI_PROVIDER)!;
|
||||
const connectionInfoResource = di.getService(ConnectionInfoResource);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
|
||||
return new ConnectionFormAccessPart(formState, connectionInfoResource, optionsPart);
|
||||
});
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, computed, observable } from 'mobx';
|
||||
|
||||
import { useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk';
|
||||
import { useTabState } from '@cloudbeaver/core-ui';
|
||||
import { type ILoadableState, isArraysEqual, isContainsException } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IConnectionAccessTabState } from './IConnectionAccessTabState.js';
|
||||
|
||||
interface State extends ILoadableState {
|
||||
state: IConnectionAccessTabState;
|
||||
changed: boolean;
|
||||
edit: () => void;
|
||||
revoke: (subjectIds: string[]) => void;
|
||||
grant: (subjectIds: string[]) => void;
|
||||
load: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useConnectionAccessState(connection: DatabaseConnectionFragment | undefined): Readonly<State> {
|
||||
const resource = useService(ConnectionInfoResource);
|
||||
const notificationService = useService(NotificationService);
|
||||
const state = useTabState<IConnectionAccessTabState>();
|
||||
|
||||
return useObservableRef(
|
||||
() => ({
|
||||
exception: null as Error | null,
|
||||
get changed() {
|
||||
return !isArraysEqual(this.state.initialGrantedSubjects, this.state.grantedSubjects);
|
||||
},
|
||||
isLoading() {
|
||||
return this.state.loading;
|
||||
},
|
||||
isError() {
|
||||
return isContainsException(this.exception);
|
||||
},
|
||||
isLoaded() {
|
||||
return this.state.loaded;
|
||||
},
|
||||
edit() {
|
||||
this.state.editing = !this.state.editing;
|
||||
},
|
||||
revoke(subjectIds: string[]) {
|
||||
this.state.grantedSubjects = this.state.grantedSubjects.filter(subject => !subjectIds.includes(subject));
|
||||
},
|
||||
grant(subjectIds: string[]) {
|
||||
this.state.grantedSubjects.push(...subjectIds);
|
||||
},
|
||||
async load(reload = false) {
|
||||
let loaded = this.exception || this.state.loaded;
|
||||
|
||||
if (reload) {
|
||||
loaded = false;
|
||||
}
|
||||
|
||||
if (loaded || this.state.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.state.loading = true;
|
||||
|
||||
if (this.connection) {
|
||||
const key = createConnectionParam(this.connection);
|
||||
const grantedSubjects = await this.resource.loadAccessSubjects(key);
|
||||
this.state.grantedSubjects = grantedSubjects.map(subject => subject.subjectId);
|
||||
this.state.initialGrantedSubjects = this.state.grantedSubjects.slice();
|
||||
}
|
||||
|
||||
this.state.loaded = true;
|
||||
this.exception = null;
|
||||
} catch (exception: any) {
|
||||
this.notificationService.logException(exception, 'connections_connection_edit_access_load_failed');
|
||||
this.exception = exception;
|
||||
} finally {
|
||||
this.state.loading = false;
|
||||
}
|
||||
},
|
||||
async reload() {
|
||||
this.load(true);
|
||||
},
|
||||
}),
|
||||
{
|
||||
exception: observable.ref,
|
||||
state: observable.ref,
|
||||
changed: computed,
|
||||
edit: action.bound,
|
||||
isLoading: action.bound,
|
||||
isLoaded: action.bound,
|
||||
reload: action.bound,
|
||||
revoke: action.bound,
|
||||
grant: action.bound,
|
||||
},
|
||||
{ state, connection, resource, notificationService },
|
||||
['load'],
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,9 @@
|
||||
{
|
||||
"path": "../core-connections"
|
||||
},
|
||||
{
|
||||
"path": "../core-data-context"
|
||||
},
|
||||
{
|
||||
"path": "../core-di"
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@cloudbeaver/plugin-navigation-tree": "workspace:*",
|
||||
"@cloudbeaver/plugin-projects": "workspace:*",
|
||||
"@cloudbeaver/plugin-top-app-bar": "workspace:*",
|
||||
"@dbeaver/jdbc-uri-parser": "workspace:^",
|
||||
"mobx": "^6",
|
||||
"mobx-react-lite": "^4",
|
||||
"react": "^19",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
*/
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { IConnectionFormState } from './IConnectionFormProps.js';
|
||||
|
||||
export interface IConnectionFormActionsContext {
|
||||
save: IConnectionFormState['save'];
|
||||
test: IConnectionFormState['test'];
|
||||
save: () => Promise<void>;
|
||||
test: () => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export const ConnectionFormActionsContext = createContext<IConnectionFormActionsContext | null>(null);
|
||||
|
||||
@@ -1,98 +1,105 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 { useEffect } from 'react';
|
||||
|
||||
import { ExceptionMessage, Form, Loader, Placeholder, s, StatusMessage, useExecutor, useForm, useObjectRef, useS } from '@cloudbeaver/core-blocks';
|
||||
import { Container, Form, Loader, Placeholder, s, StatusMessage, useForm, useObjectRef, useResource, useS } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { ENotificationType } from '@cloudbeaver/core-events';
|
||||
import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
import { TabList, TabPanelList, TabsState } from '@cloudbeaver/core-ui';
|
||||
import { formSubmitContext, TabList, TabPanelList, TabsState, type IFormState } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { ConnectionFormActionsContext, type IConnectionFormActionsContext } from './ConnectFormActionsContext.js';
|
||||
import style from './ConnectionForm.module.css';
|
||||
import type { ConnectionFormState } from './ConnectionFormState.js';
|
||||
import { getFirstException } from '@cloudbeaver/core-utils';
|
||||
import { ConnectionFormService } from './ConnectionFormService.js';
|
||||
import { connectionConfigContext } from './Contexts/connectionConfigContext.js';
|
||||
import type { IConnectionFormState } from './IConnectionFormProps.js';
|
||||
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { getConnectionFormOptionsPart } from './Options/getConnectionFormOptionsPart.js';
|
||||
import { ExecutionContext } from '@cloudbeaver/core-executor';
|
||||
import type { IConnectionFormState } from './IConnectionFormState.js';
|
||||
|
||||
export interface ConnectionFormProps {
|
||||
state: IConnectionFormState;
|
||||
formState: ConnectionFormState;
|
||||
onCancel?: () => void;
|
||||
onSave?: (config: ConnectionConfig) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ConnectionForm = observer<ConnectionFormProps>(function ConnectionForm({ state, onCancel, onSave = () => {}, className }) {
|
||||
const props = useObjectRef({ onSave });
|
||||
export const ConnectionForm = observer<ConnectionFormProps>(function ConnectionForm({ formState, onCancel, onSave = () => {}, className }) {
|
||||
const service = useService(ConnectionFormService);
|
||||
const styles = useS(style);
|
||||
const notificationService = useService(NotificationService);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const connectionInfoResource = useResource(ConnectionForm, ConnectionInfoResource, optionsPart.connectionKey, {
|
||||
active: !!optionsPart.connectionKey,
|
||||
});
|
||||
const exception = getFirstException(formState.exception);
|
||||
|
||||
const form = useForm({
|
||||
onSubmit: event => {
|
||||
if (event?.type === 'test') {
|
||||
state.test();
|
||||
onSubmit: async event => {
|
||||
const submitType = event?.type === 'test' ? 'test' : 'submit';
|
||||
const context = new ExecutionContext<IFormState<IConnectionFormState>>(formState);
|
||||
|
||||
const submitInfo = context.getContext(formSubmitContext);
|
||||
submitInfo.setType(submitType);
|
||||
if (submitType === 'test') {
|
||||
submitInfo.setSubmitOnNoChanges(true);
|
||||
}
|
||||
const initialMode = formState.mode;
|
||||
const saved = await formState.save(context);
|
||||
|
||||
if (saved) {
|
||||
if (submitType === 'submit') {
|
||||
notificationService.notify(
|
||||
{
|
||||
title: initialMode === 'create' ? 'core_connections_connection_create_success' : 'core_connections_connection_update_success',
|
||||
message: connectionInfoResource.data?.name,
|
||||
},
|
||||
ENotificationType.Success,
|
||||
);
|
||||
|
||||
onSave(optionsPart.state);
|
||||
}
|
||||
} else {
|
||||
state.save();
|
||||
const error = getFirstException(formState.exception);
|
||||
|
||||
if (submitType === 'submit') {
|
||||
notificationService.logException(error, 'connections_connection_create_fail');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const actionsContext = useObjectRef<IConnectionFormActionsContext>(() => ({
|
||||
save: async () => form.submit(new SubmitEvent('submit')),
|
||||
test: async () => form.submit(new SubmitEvent('test')),
|
||||
save: () => form.submit(new SubmitEvent('submit')),
|
||||
test: () => form.submit(new SubmitEvent('test')),
|
||||
onCancel,
|
||||
}));
|
||||
|
||||
useExecutor({
|
||||
executor: state.submittingTask,
|
||||
postHandlers: [
|
||||
function save(data, contexts) {
|
||||
const validation = contexts.getContext(service.connectionValidationContext);
|
||||
const state = contexts.getContext(service.connectionStatusContext);
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
|
||||
if (validation.valid && state.saved && data.submitType === 'submit') {
|
||||
props.onSave(config);
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
state.loadConnectionInfo();
|
||||
}, [state]);
|
||||
|
||||
if (state.initError) {
|
||||
return <ExceptionMessage exception={state.initError} onRetry={() => state.loadConnectionInfo()} />;
|
||||
}
|
||||
|
||||
if (!state.configured) {
|
||||
return (
|
||||
<div className={s(styles, { box: true }, className)}>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form context={form} contents>
|
||||
<TabsState container={service.tabsContainer} localState={state.partsState} state={state} onCancel={onCancel}>
|
||||
<TabsState container={service.parts} localState={formState.parts} formState={formState}>
|
||||
<div className={s(styles, { box: true }, className)}>
|
||||
<div className={s(styles, { connectionTopBar: true })}>
|
||||
<div className={s(styles, { connectionTopBarTabs: true })}>
|
||||
<div className={s(styles, { connectionStatusMessage: true })}>
|
||||
<StatusMessage type={ENotificationType.Info} message={state.statusMessage} />
|
||||
</div>
|
||||
<TabList className={s(styles, { tabList: true })} disabled={state.disabled} underline big />
|
||||
</div>
|
||||
<Container className={s(styles, { connectionTopBarTabs: true })} overflow>
|
||||
<Container className={s(styles, { connectionStatusMessage: true })} overflow>
|
||||
<StatusMessage
|
||||
type={exception ? ENotificationType.Error : ENotificationType.Info}
|
||||
message={formState.statusMessage}
|
||||
exception={exception}
|
||||
/>
|
||||
</Container>
|
||||
<TabList className={s(styles, { tabList: true })} disabled={formState.isDisabled} underline big />
|
||||
</Container>
|
||||
<div className={s(styles, { connectionTopBarActions: true })}>
|
||||
<Loader suspense inline hideMessage hideException>
|
||||
<ConnectionFormActionsContext.Provider value={actionsContext}>
|
||||
<Placeholder container={service.actionsContainer} state={state} onCancel={onCancel} />
|
||||
<Placeholder container={service.actionsContainer} formState={formState} />
|
||||
</ConnectionFormActionsContext.Provider>
|
||||
</Loader>
|
||||
</div>
|
||||
|
||||
+26
-18
@@ -1,25 +1,23 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 { useContext } from 'react';
|
||||
|
||||
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
|
||||
import { Button, getComputed, type PlaceholderComponent, useResource, useTranslate, useAuthenticationAction } from '@cloudbeaver/core-blocks';
|
||||
import { DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { ConnectionInfoResource, DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
|
||||
import { ConnectionFormActionsContext } from './ConnectFormActionsContext.js';
|
||||
import type { IConnectionFormProps } from './IConnectionFormProps.js';
|
||||
|
||||
export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProps> = observer(function ConnectionFormBaseActions({
|
||||
state,
|
||||
onCancel,
|
||||
}) {
|
||||
import type { IConnectionFormProps } from './IConnectionFormState.js';
|
||||
import { getConnectionFormOptionsPart } from './Options/getConnectionFormOptionsPart.js';
|
||||
export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProps> = observer(function ConnectionFormBaseActions({ formState }) {
|
||||
const actions = useContext(ConnectionFormActionsContext);
|
||||
|
||||
if (!actions) {
|
||||
@@ -27,36 +25,46 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
|
||||
}
|
||||
|
||||
const translate = useTranslate();
|
||||
const driverMap = useResource(ConnectionFormBaseActions, DBDriverResource, state.config.driverId || null);
|
||||
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const driverMap = useResource(ConnectionFormBaseActions, DBDriverResource, optionsPart.state.driverId || null);
|
||||
const driver = driverMap.data;
|
||||
const connectionInfoResource = useResource(ConnectionFormBaseActions, ConnectionInfoResource, optionsPart.connectionKey, {
|
||||
active: !!optionsPart.connectionKey,
|
||||
});
|
||||
|
||||
const serverConfigResource = useResource(ConnectionFormBaseActions, ServerConfigResource, undefined);
|
||||
const { data: authModel } = useResource(
|
||||
ConnectionFormBaseActions,
|
||||
DatabaseAuthModelsResource,
|
||||
getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null),
|
||||
getComputed(() => optionsPart.state.authModelId || connectionInfoResource.data?.authModel || driver?.defaultAuthModel || null),
|
||||
);
|
||||
const authentication = useAuthenticationAction({
|
||||
providerId: authModel?.requiredAuth ?? state.info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID,
|
||||
providerId: authModel?.requiredAuth ?? connectionInfoResource.data?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID,
|
||||
});
|
||||
|
||||
const authorized = authentication.providerId === AUTH_PROVIDER_LOCAL_ID || authentication.authorized;
|
||||
const disableTest = serverConfigResource.data?.distributed && !!state.config.sharedCredentials;
|
||||
const disableTest = serverConfigResource.data?.distributed && !!optionsPart.state.sharedCredentials;
|
||||
|
||||
return (
|
||||
<>
|
||||
{onCancel && (
|
||||
<Button type="button" disabled={state.disabled} mod={['outlined']} onClick={onCancel}>
|
||||
{actions.onCancel && (
|
||||
<Button type="button" disabled={formState.isDisabled} mod={['outlined']} onClick={actions.onCancel}>
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
)}
|
||||
{!disableTest && (
|
||||
<Button type="button" disabled={state.disabled || !authorized} mod={['outlined']} loader onClick={actions['test']}>
|
||||
<Button type="button" disabled={formState.isDisabled || !authorized} mod={['outlined']} loader onClick={actions['test']}>
|
||||
{translate('connections_connection_test')}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" disabled={state.disabled || state.readonly} mod={['unelevated']} loader onClick={actions['save']}>
|
||||
{translate(state.mode === 'edit' ? 'ui_processing_save' : 'ui_processing_create')}
|
||||
<Button
|
||||
type="button"
|
||||
disabled={formState.isDisabled || formState.isReadOnly || !formState.isChanged}
|
||||
mod={['unelevated']}
|
||||
loader
|
||||
onClick={actions['save']}
|
||||
>
|
||||
{translate(formState.mode === 'edit' ? 'ui_processing_save' : 'ui_processing_create')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,182 +1,24 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 { observable, runInAction, toJS } from 'mobx';
|
||||
|
||||
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
|
||||
import { ExecutorHandlersCollection, ExecutorInterrupter, type IExecutorHandler, type IExecutorHandlersCollection } from '@cloudbeaver/core-executor';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import { TabsContainer } from '@cloudbeaver/core-ui';
|
||||
import { FormBaseService } from '@cloudbeaver/core-ui';
|
||||
import type { IConnectionFormProps, IConnectionFormState } from './IConnectionFormState.js';
|
||||
import { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import { ConnectionAuthenticationDialogLoader } from '../ConnectionAuthentication/ConnectionAuthenticationDialogLoader.js';
|
||||
import { ConnectionFormBaseActionsLoader } from './ConnectionFormBaseActionsLoader.js';
|
||||
import { connectionConfigContext } from './Contexts/connectionConfigContext.js';
|
||||
import { connectionCredentialsStateContext } from './Contexts/connectionCredentialsStateContext.js';
|
||||
import type { IConnectionFormFillConfigData, IConnectionFormProps, IConnectionFormState, IConnectionFormSubmitData } from './IConnectionFormProps.js';
|
||||
|
||||
export interface IConnectionFormValidation {
|
||||
valid: boolean;
|
||||
messages: string[];
|
||||
info: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
}
|
||||
|
||||
export interface IConnectionFormStatus {
|
||||
saved: boolean;
|
||||
messages: string[];
|
||||
exception: Error | null;
|
||||
info: (message: string) => void;
|
||||
error: (message: string, exception?: Error) => void;
|
||||
}
|
||||
const ConnectionFormBaseActionsLoader = importLazyComponent(() => import('./ConnectionFormBaseActions.js').then(m => m.ConnectionFormBaseActions));
|
||||
|
||||
@injectable()
|
||||
export class ConnectionFormService {
|
||||
readonly tabsContainer: TabsContainer<IConnectionFormProps>;
|
||||
readonly actionsContainer: PlaceholderContainer<IConnectionFormProps>;
|
||||
|
||||
readonly configureTask: IExecutorHandlersCollection<IConnectionFormState>;
|
||||
readonly fillConfigTask: IExecutorHandlersCollection<IConnectionFormFillConfigData>;
|
||||
readonly prepareConfigTask: IExecutorHandlersCollection<IConnectionFormSubmitData>;
|
||||
readonly formValidationTask: IExecutorHandlersCollection<IConnectionFormSubmitData>;
|
||||
readonly formSubmittingTask: IExecutorHandlersCollection<IConnectionFormSubmitData>;
|
||||
readonly formStateTask: IExecutorHandlersCollection<IConnectionFormState>;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
private readonly localizationService: LocalizationService,
|
||||
) {
|
||||
this.tabsContainer = new TabsContainer('Connection settings');
|
||||
this.actionsContainer = new PlaceholderContainer();
|
||||
this.configureTask = new ExecutorHandlersCollection();
|
||||
this.fillConfigTask = new ExecutorHandlersCollection();
|
||||
this.prepareConfigTask = new ExecutorHandlersCollection();
|
||||
this.formSubmittingTask = new ExecutorHandlersCollection();
|
||||
this.formValidationTask = new ExecutorHandlersCollection();
|
||||
this.formStateTask = new ExecutorHandlersCollection();
|
||||
|
||||
this.formSubmittingTask.before(this.formValidationTask).before(this.prepareConfigTask);
|
||||
|
||||
this.formStateTask.before<IConnectionFormSubmitData>(this.prepareConfigTask, state => ({ state, submitType: 'submit' }));
|
||||
|
||||
this.prepareConfigTask.addPostHandler(this.askCredentials);
|
||||
this.formSubmittingTask.addPostHandler(this.showSubmittingStatusMessage);
|
||||
this.formValidationTask.addPostHandler(this.ensureValidation);
|
||||
export class ConnectionFormService extends FormBaseService<IConnectionFormState, IConnectionFormProps> {
|
||||
constructor(localizationService: LocalizationService, notificationService: NotificationService) {
|
||||
super(localizationService, notificationService, 'Connection form');
|
||||
|
||||
this.actionsContainer.add(ConnectionFormBaseActionsLoader);
|
||||
}
|
||||
|
||||
connectionValidationContext = (): IConnectionFormValidation => ({
|
||||
valid: true,
|
||||
messages: [],
|
||||
info(message: string) {
|
||||
this.messages.push(message);
|
||||
},
|
||||
error(message: string) {
|
||||
this.messages.push(message);
|
||||
this.valid = false;
|
||||
},
|
||||
});
|
||||
|
||||
connectionStatusContext = (): IConnectionFormStatus => ({
|
||||
saved: true,
|
||||
messages: [],
|
||||
exception: null,
|
||||
info(message: string) {
|
||||
this.messages.push(message);
|
||||
},
|
||||
error(message: string, exception: Error | null = null) {
|
||||
this.messages.push(message);
|
||||
this.saved = false;
|
||||
this.exception = exception;
|
||||
},
|
||||
});
|
||||
|
||||
private readonly showSubmittingStatusMessage: IExecutorHandler<IConnectionFormSubmitData> = (data, contexts) => {
|
||||
const status = contexts.getContext(this.connectionStatusContext);
|
||||
|
||||
if (!status.saved) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
}
|
||||
|
||||
if (status.messages.length > 0) {
|
||||
if (status.exception) {
|
||||
this.notificationService.logException(status.exception, status.messages[0], status.messages.slice(1).join('\n'));
|
||||
} else {
|
||||
this.notificationService.notify(
|
||||
{
|
||||
title: status.messages[0]!,
|
||||
message: status.messages.slice(1).join('\n'),
|
||||
},
|
||||
status.saved ? ENotificationType.Success : ENotificationType.Error,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private readonly askCredentials: IExecutorHandler<IConnectionFormSubmitData> = async (data, contexts) => {
|
||||
const credentialsState = contexts.getContext(connectionCredentialsStateContext);
|
||||
|
||||
if (data.submitType !== 'test' || (!credentialsState.authModelId && !credentialsState.networkHandlers.length)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
|
||||
runInAction(() => {
|
||||
if (credentialsState.authModelId) {
|
||||
if (!config.credentials) {
|
||||
config.credentials = { ...data.state.config.credentials };
|
||||
}
|
||||
|
||||
config.credentials = observable(config.credentials);
|
||||
}
|
||||
|
||||
if (credentialsState.networkHandlers.length > 0) {
|
||||
if (!config.networkHandlersConfig) {
|
||||
config.networkHandlersConfig = toJS(data.state.config.networkHandlersConfig) || [];
|
||||
}
|
||||
|
||||
config.networkHandlersConfig = observable(config.networkHandlersConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const result = await this.commonDialogService.open(ConnectionAuthenticationDialogLoader, {
|
||||
config,
|
||||
authModelId: credentialsState.authModelId,
|
||||
networkHandlers: credentialsState.networkHandlers,
|
||||
projectId: data.state.projectId,
|
||||
});
|
||||
|
||||
if (result === DialogueStateResult.Rejected) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly ensureValidation: IExecutorHandler<IConnectionFormSubmitData> = (data, contexts) => {
|
||||
const validation = contexts.getContext(this.connectionValidationContext);
|
||||
|
||||
if (!validation.valid) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
}
|
||||
|
||||
if (validation.messages.length > 0) {
|
||||
const messages = validation.messages.map(message => this.localizationService.translate(message));
|
||||
this.notificationService.notify(
|
||||
{
|
||||
title:
|
||||
data.state.mode === 'edit' ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error',
|
||||
message: messages.join('\n'),
|
||||
},
|
||||
validation.valid ? ENotificationType.Info : ENotificationType.Error,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,342 +1,18 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import type { IServiceProvider } from '@cloudbeaver/core-di';
|
||||
import { FormState } from '@cloudbeaver/core-ui';
|
||||
|
||||
import {
|
||||
ConnectionInfoOriginResource,
|
||||
ConnectionInfoResource,
|
||||
createConnectionParam,
|
||||
type DatabaseConnection,
|
||||
type IConnectionInfoParams,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { Executor, type IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor';
|
||||
import type { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import type { ResourceKeySimple } from '@cloudbeaver/core-resource';
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
import { formStateContext, type IFormStateInfo } from '@cloudbeaver/core-ui';
|
||||
import { MetadataMap, uuid } from '@cloudbeaver/core-utils';
|
||||
import { ConnectionFormService } from './ConnectionFormService.js';
|
||||
import type { IConnectionFormState } from './IConnectionFormState.js';
|
||||
|
||||
import { connectionFormConfigureContext } from './connectionFormConfigureContext.js';
|
||||
import type { ConnectionFormService } from './ConnectionFormService.js';
|
||||
import type { ConnectionFormMode, ConnectionFormType, IConnectionFormState, IConnectionFormSubmitData } from './IConnectionFormProps.js';
|
||||
|
||||
export class ConnectionFormState implements IConnectionFormState {
|
||||
mode: ConnectionFormMode;
|
||||
type: ConnectionFormType;
|
||||
projectId: string | null;
|
||||
|
||||
config: ConnectionConfig;
|
||||
|
||||
partsState: MetadataMap<string, any>;
|
||||
|
||||
statusMessage: string | string[] | null;
|
||||
configured: boolean;
|
||||
initError: Error | null;
|
||||
|
||||
get loading(): boolean {
|
||||
return this.loadConnectionTask.executing || this.submittingTask.executing;
|
||||
}
|
||||
|
||||
get disabled(): boolean {
|
||||
return this.loading || !!this.stateInfo?.disabled || this.loadConnectionTask.executing;
|
||||
}
|
||||
|
||||
get availableDrivers(): string[] {
|
||||
if (this._availableDrivers.length === 0 && this.config.driverId) {
|
||||
return [this.config.driverId];
|
||||
}
|
||||
|
||||
return this._availableDrivers;
|
||||
}
|
||||
|
||||
get info(): DatabaseConnection | undefined {
|
||||
if (!this.config.connectionId || this.projectId === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.resource.get(createConnectionParam(this.projectId, this.config.connectionId));
|
||||
}
|
||||
|
||||
get originInfo() {
|
||||
if (!this.config.connectionId || this.projectId === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.originResource.get(createConnectionParam(this.projectId, this.config.connectionId));
|
||||
}
|
||||
|
||||
get readonly(): boolean {
|
||||
if (this.stateInfo?.readonly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.type === 'admin' || this.mode === 'create') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.info?.canEdit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
if (this.mode === 'create') {
|
||||
return 'create';
|
||||
}
|
||||
|
||||
return this.config.connectionId || this._id;
|
||||
}
|
||||
|
||||
readonly resource: ConnectionInfoResource;
|
||||
readonly originResource: ConnectionInfoOriginResource;
|
||||
readonly service: ConnectionFormService;
|
||||
readonly submittingTask: IExecutor<IConnectionFormSubmitData>;
|
||||
readonly closeTask: IExecutor;
|
||||
|
||||
private readonly _id: string;
|
||||
private stateInfo: IFormStateInfo | null;
|
||||
private readonly loadConnectionTask: IExecutor<IConnectionFormState>;
|
||||
private readonly formStateTask: IExecutor<IConnectionFormState>;
|
||||
private _availableDrivers: string[];
|
||||
|
||||
constructor(
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly projectInfoResource: ProjectInfoResource,
|
||||
service: ConnectionFormService,
|
||||
resource: ConnectionInfoResource,
|
||||
originResource: ConnectionInfoOriginResource,
|
||||
) {
|
||||
this._id = uuid();
|
||||
this.initError = null;
|
||||
|
||||
this.resource = resource;
|
||||
this.originResource = originResource;
|
||||
this.projectId = null;
|
||||
this.config = {};
|
||||
this._availableDrivers = [];
|
||||
this.stateInfo = null;
|
||||
this.partsState = new MetadataMap();
|
||||
this.service = service;
|
||||
this.formStateTask = new Executor<IConnectionFormState>(this, () => true);
|
||||
this.loadConnectionTask = new Executor<IConnectionFormState>(this, () => true);
|
||||
this.submittingTask = new Executor();
|
||||
this.closeTask = new Executor();
|
||||
this.statusMessage = null;
|
||||
this.configured = false;
|
||||
this.mode = 'create';
|
||||
this.type = 'public';
|
||||
|
||||
this.syncProject = this.syncProject.bind(this);
|
||||
this.syncInfo = this.syncInfo.bind(this);
|
||||
this.test = this.test.bind(this);
|
||||
this.save = this.save.bind(this);
|
||||
this.checkFormState = this.checkFormState.bind(this);
|
||||
this.loadInfo = this.loadInfo.bind(this);
|
||||
this.updateFormState = this.updateFormState.bind(this);
|
||||
|
||||
this.formStateTask.addCollection(service.formStateTask).addPostHandler(this.updateFormState);
|
||||
|
||||
this.resource.onItemUpdate.addHandler(this.syncInfo);
|
||||
|
||||
this.projectInfoResource.onDataUpdate.addHandler(this.syncProject);
|
||||
|
||||
this.projectsService.onActiveProjectChange.addHandler(this.syncProject);
|
||||
|
||||
this.submittingTask.addPostHandler(async (data, contexts) => {
|
||||
const status = contexts.getContext(service.connectionStatusContext);
|
||||
const validation = contexts.getContext(service.connectionValidationContext);
|
||||
|
||||
if (data.submitType !== 'submit' || !status.saved || !validation.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.loadConnectionTask
|
||||
.before(service.configureTask)
|
||||
.addPostHandler(this.loadInfo)
|
||||
.next(service.fillConfigTask, (state, contexts) => {
|
||||
const configuration = contexts.getContext(connectionFormConfigureContext);
|
||||
|
||||
return {
|
||||
state,
|
||||
updated: state.info !== configuration.info || state.config.driverId !== configuration.driverId || !this.configured,
|
||||
};
|
||||
})
|
||||
.next(this.formStateTask);
|
||||
|
||||
makeObservable<IConnectionFormState, '_availableDrivers' | 'stateInfo' | 'updateFormState'>(this, {
|
||||
projectId: observable,
|
||||
mode: observable,
|
||||
type: observable,
|
||||
config: observable,
|
||||
availableDrivers: computed,
|
||||
_availableDrivers: observable,
|
||||
info: computed,
|
||||
originInfo: computed,
|
||||
statusMessage: observable,
|
||||
configured: observable,
|
||||
readonly: computed,
|
||||
stateInfo: observable,
|
||||
initError: observable.ref,
|
||||
id: computed,
|
||||
reset: action,
|
||||
setOptions: action,
|
||||
setConfig: action,
|
||||
setAvailableDrivers: action,
|
||||
updateFormState: action,
|
||||
});
|
||||
}
|
||||
|
||||
async loadConnectionInfo(): Promise<DatabaseConnection | undefined> {
|
||||
try {
|
||||
this.initError = null;
|
||||
await this.loadConnectionTask.execute(this);
|
||||
|
||||
return this.info;
|
||||
} catch (exception: any) {
|
||||
this.initError = exception;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
await this.loadConnectionInfo();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.configured = false;
|
||||
this.partsState.clear();
|
||||
}
|
||||
|
||||
setPartsState(state: MetadataMap<string, any>): this {
|
||||
this.partsState = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
setOptions(mode: ConnectionFormMode, type: ConnectionFormType): this {
|
||||
this.mode = mode;
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
setConfig(projectId: string, config: ConnectionConfig): this {
|
||||
this.setProject(projectId);
|
||||
this.config = config;
|
||||
this.reset();
|
||||
return this;
|
||||
}
|
||||
|
||||
setProject(projectId: string): this {
|
||||
this.projectId = projectId;
|
||||
return this;
|
||||
}
|
||||
|
||||
setAvailableDrivers(drivers: string[]): this {
|
||||
this._availableDrivers = drivers;
|
||||
this.reset();
|
||||
return this;
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
await this.submittingTask.executeScope(
|
||||
{
|
||||
state: this,
|
||||
submitType: 'submit',
|
||||
},
|
||||
this.service.formSubmittingTask,
|
||||
);
|
||||
}
|
||||
|
||||
async test(): Promise<void> {
|
||||
await this.submittingTask.executeScope(
|
||||
{
|
||||
state: this,
|
||||
submitType: 'test',
|
||||
},
|
||||
this.service.formSubmittingTask,
|
||||
);
|
||||
}
|
||||
|
||||
async checkFormState(): Promise<IFormStateInfo | null> {
|
||||
await this.loadConnectionInfo();
|
||||
return this.stateInfo;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.resource.onItemUpdate.removeHandler(this.syncInfo);
|
||||
this.projectInfoResource.onDataUpdate.removeHandler(this.syncProject);
|
||||
this.projectsService.onActiveProjectChange.removeHandler(this.syncProject);
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.closeTask.execute();
|
||||
}
|
||||
|
||||
private updateFormState(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>): void {
|
||||
const context = contexts.getContext(formStateContext);
|
||||
|
||||
if (this.mode === 'create') {
|
||||
context.markEdited();
|
||||
}
|
||||
|
||||
this.statusMessage = context.statusMessage;
|
||||
|
||||
if (this.statusMessage === null && this.mode === 'edit') {
|
||||
if (!this.info?.canEdit) {
|
||||
this.statusMessage = 'connections_connection_edit_not_own_deny';
|
||||
}
|
||||
}
|
||||
|
||||
this.stateInfo = context;
|
||||
this.configured = true;
|
||||
}
|
||||
|
||||
private syncInfo(key: ResourceKeySimple<IConnectionInfoParams>) {
|
||||
if (
|
||||
!this.config.connectionId ||
|
||||
this.projectId === null ||
|
||||
!this.resource.isIntersect(key, createConnectionParam(this.projectId, this.config.connectionId))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadConnectionInfo();
|
||||
}
|
||||
|
||||
private async syncProject() {
|
||||
if (!this.projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const project = this.projectInfoResource.get(this.projectId);
|
||||
if (!project?.canEditDataSources || !this.projectsService.activeProjects.includes(project)) {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadInfo(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
if (!data.config.connectionId || data.projectId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = createConnectionParam(data.projectId, data.config.connectionId);
|
||||
const configuration = contexts.getContext(connectionFormConfigureContext);
|
||||
|
||||
if (!data.resource.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await data.resource.load(key, configuration.connectionIncludes);
|
||||
await this.originResource.load(key);
|
||||
export class ConnectionFormState extends FormState<IConnectionFormState> {
|
||||
constructor(serviceProvider: IServiceProvider, service: ConnectionFormService, config: IConnectionFormState) {
|
||||
super(serviceProvider, service, config);
|
||||
}
|
||||
}
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
|
||||
export function connectionConfigContext(): ConnectionConfig {
|
||||
return {};
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
interface IConnectionCredentialsState {
|
||||
authModelId: string | null;
|
||||
networkHandlers: string[];
|
||||
}
|
||||
|
||||
interface IConnectionCredentialsStateContext extends IConnectionCredentialsState {
|
||||
requireAuthModel: (id: string) => void;
|
||||
requireNetworkHandler: (id: string) => void;
|
||||
}
|
||||
|
||||
export function connectionCredentialsStateContext(): IConnectionCredentialsStateContext {
|
||||
return {
|
||||
authModelId: null,
|
||||
networkHandlers: [],
|
||||
requireAuthModel(id) {
|
||||
this.authModelId = id;
|
||||
},
|
||||
requireNetworkHandler(id) {
|
||||
this.networkHandlers.push(id);
|
||||
},
|
||||
};
|
||||
}
|
||||
+13
-95
@@ -1,122 +1,40 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, makeObservable } from 'mobx';
|
||||
|
||||
import { DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { isObjectPropertyInfoStateEqual } from '@cloudbeaver/core-sdk';
|
||||
import { formStateContext } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js';
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import { connectionConfigContext } from '../Contexts/connectionConfigContext.js';
|
||||
import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps.js';
|
||||
import { DriverPropertiesLoader } from './DriverPropertiesLoader.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
import { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
|
||||
const DriverProperties = importLazyComponent(() => import('./DriverProperties.js').then(m => m.DriverProperties));
|
||||
|
||||
@injectable()
|
||||
export class ConnectionDriverPropertiesTabService extends Bootstrap {
|
||||
constructor(
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly dbDriverResource: DBDriverResource,
|
||||
) {
|
||||
constructor(private readonly connectionFormService: ConnectionFormService) {
|
||||
super();
|
||||
|
||||
makeObservable<this, 'fillConfig'>(this, {
|
||||
fillConfig: action,
|
||||
});
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.tabsContainer.add({
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'driver_properties',
|
||||
name: 'plugin_connections_connection_form_part_properties',
|
||||
title: 'plugin_connections_connection_form_part_properties',
|
||||
order: 2,
|
||||
panel: () => DriverPropertiesLoader,
|
||||
panel: () => DriverProperties,
|
||||
isDisabled: (tabId, props) => {
|
||||
if (props?.state.config.driverId) {
|
||||
return !props.state.config.driverId;
|
||||
const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null;
|
||||
|
||||
if (optionsPart?.state.driverId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this));
|
||||
|
||||
this.connectionFormService.formStateTask.addHandler(this.formState.bind(this));
|
||||
|
||||
this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this));
|
||||
|
||||
this.connectionFormService.configureTask.addHandler(this.configure.bind(this));
|
||||
}
|
||||
|
||||
private configure(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const configuration = contexts.getContext(connectionFormConfigureContext);
|
||||
|
||||
configuration.include('includeProperties', 'includeProviderProperties');
|
||||
}
|
||||
|
||||
private fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider<IConnectionFormFillConfigData>) {
|
||||
if (!updated) {
|
||||
return;
|
||||
}
|
||||
if (!state.config.properties) {
|
||||
state.config.properties = {};
|
||||
}
|
||||
|
||||
if (!state.info) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.config.properties = { ...state.info.properties };
|
||||
}
|
||||
|
||||
private prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider<IConnectionFormSubmitData>) {
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
|
||||
config.properties = { ...state.config.properties };
|
||||
|
||||
if (config.driverId) {
|
||||
const driver = this.dbDriverResource.get(config.driverId);
|
||||
const trimmedProperties: typeof config.properties = {};
|
||||
|
||||
const defaultDriverProperties = new Set(driver?.driverProperties?.map(property => property.id) ?? []);
|
||||
|
||||
for (let key of Object.keys(config.properties)) {
|
||||
const value = config.properties[key];
|
||||
if (!defaultDriverProperties?.has(key)) {
|
||||
key = key.trim();
|
||||
}
|
||||
|
||||
trimmedProperties[key] = typeof value === 'string' ? value.trim() : value;
|
||||
}
|
||||
|
||||
config.properties = trimmedProperties;
|
||||
}
|
||||
}
|
||||
|
||||
private formState(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
if (!data.info || !data.config.driverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
const driver = this.dbDriverResource.get(data.config.driverId);
|
||||
|
||||
if (!driver?.driverProperties) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isObjectPropertyInfoStateEqual(driver.driverProperties, config.properties, data.info.properties)) {
|
||||
const stateContext = contexts.getContext(formStateContext);
|
||||
|
||||
stateContext.markEdited();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { FormPart, type IFormState } from '@cloudbeaver/core-ui';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { ConnectionInfoPropertiesResource } from '@cloudbeaver/core-connections';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import type { IConnectionProperties } from '../Options/IConnectionConfig.js';
|
||||
import { observable, runInAction } from 'mobx';
|
||||
import type { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js';
|
||||
|
||||
function getDefaultState(): IConnectionProperties {
|
||||
return {};
|
||||
}
|
||||
|
||||
export class ConnectionFormDriverPropertiesPart extends FormPart<IConnectionProperties, IConnectionFormState> {
|
||||
constructor(
|
||||
formState: IFormState<IConnectionFormState>,
|
||||
private readonly connectionInfoPropertiesResource: ConnectionInfoPropertiesResource,
|
||||
private readonly optionsPart: ConnectionFormOptionsPart,
|
||||
) {
|
||||
super(formState, getDefaultState());
|
||||
|
||||
this.optionsPart.onDriverIdChange.addHandler(this.onDriverIdChangeHandler.bind(this));
|
||||
}
|
||||
|
||||
private onDriverIdChangeHandler(driverId: string | undefined) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
override isOutdated(): boolean {
|
||||
if (!this.optionsPart.connectionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.connectionInfoPropertiesResource.isOutdated(this.optionsPart.connectionKey);
|
||||
}
|
||||
|
||||
protected override async loader(): Promise<void> {
|
||||
if (!this.optionsPart.connectionKey) {
|
||||
this.setInitialState(getDefaultState());
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = await this.connectionInfoPropertiesResource.load(this.optionsPart.connectionKey);
|
||||
|
||||
this.setInitialState({ ...connection.properties });
|
||||
}
|
||||
|
||||
protected override async saveChanges(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {}
|
||||
|
||||
protected override format(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): void | Promise<void> {
|
||||
runInAction(() => {
|
||||
for (const key of Object.keys(this.state!)) {
|
||||
if (typeof this.state[key] === 'string') {
|
||||
this.state[key] = this.state[key].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.optionsPart.state.properties) {
|
||||
this.optionsPart.state.properties = observable({});
|
||||
}
|
||||
|
||||
Object.assign(this.optionsPart.state.properties, this.state);
|
||||
});
|
||||
}
|
||||
}
|
||||
+15
-10
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -9,18 +9,21 @@ import { computed, observable, runInAction } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { ColoredContainer, Group, type IProperty, PropertiesTable, s, useResource, useS } from '@cloudbeaver/core-blocks';
|
||||
import { ColoredContainer, Group, type IProperty, PropertiesTable, s, useAutoLoad, useResource, useS } from '@cloudbeaver/core-blocks';
|
||||
import { DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IConnectionFormProps } from '../IConnectionFormProps.js';
|
||||
import styles from './DriverProperties.module.css';
|
||||
import { getConnectionFormDriverPropertiesPart } from './getConnectionFormDriverPropertiesPart.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
export const DriverProperties: TabContainerPanelComponent<IConnectionFormProps> = observer(function DriverProperties({ tabId, state: formState }) {
|
||||
export const DriverProperties: TabContainerPanelComponent<IConnectionFormProps> = observer(function DriverProperties({ tabId, formState }) {
|
||||
const { selected } = useTab(tabId);
|
||||
const style = useS(styles);
|
||||
|
||||
const driverPropertiesPart = getConnectionFormDriverPropertiesPart(formState);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const [state] = useState(() => {
|
||||
const propertiesList: IProperty[] = observable([]);
|
||||
|
||||
@@ -42,18 +45,18 @@ export const DriverProperties: TabContainerPanelComponent<IConnectionFormProps>
|
||||
});
|
||||
|
||||
const driver = useResource(DriverProperties, DBDriverResource, {
|
||||
key: (selected && formState.config.driverId) || null,
|
||||
key: (selected && optionsPart.state.driverId) || null,
|
||||
includes: ['includeDriverProperties'] as const,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (driver.data) {
|
||||
for (const key of Object.keys(formState.config.properties)) {
|
||||
for (const key of Object.keys(driverPropertiesPart.state)) {
|
||||
if (driver.data.driverProperties.some(property => property.id === key) || state.propertiesList.some(property => property.key === key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.add(key, formState.config.properties[key]);
|
||||
state.add(key, driverPropertiesPart.state[key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -78,14 +81,16 @@ export const DriverProperties: TabContainerPanelComponent<IConnectionFormProps>
|
||||
[driver.data],
|
||||
);
|
||||
|
||||
useAutoLoad(DriverProperties, driverPropertiesPart, selected);
|
||||
|
||||
return (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent>
|
||||
<Group className={s(style, { group: true })} box large>
|
||||
<PropertiesTable
|
||||
className={s(style, { propertiesTable: true })}
|
||||
properties={joinedProperties.get()}
|
||||
propertiesState={formState.config.properties}
|
||||
readOnly={formState.readonly}
|
||||
propertiesState={driverPropertiesPart.state}
|
||||
readOnly={formState.isDisabled || formState.isReadOnly}
|
||||
filterable
|
||||
onAdd={state.add}
|
||||
onRemove={state.remove}
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
export const DriverPropertiesLoader = React.lazy(async () => {
|
||||
const { DriverProperties } = await import('./DriverProperties.js');
|
||||
return { default: DriverProperties };
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context';
|
||||
import type { IFormState } from '@cloudbeaver/core-ui';
|
||||
import { ConnectionFormDriverPropertiesPart } from './ConnectionFormDriverPropertiesPart.js';
|
||||
import { ConnectionInfoPropertiesResource } from '@cloudbeaver/core-connections';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
const DATA_CONTEXT_CONNECTION_FORM_DRIVER_PROPERTIES_PART = createDataContext<ConnectionFormDriverPropertiesPart>(
|
||||
'Connection Form Driver Properties Part',
|
||||
);
|
||||
|
||||
export function getConnectionFormDriverPropertiesPart(formState: IFormState<IConnectionFormState>): ConnectionFormDriverPropertiesPart {
|
||||
return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_DRIVER_PROPERTIES_PART, context => {
|
||||
const di = context.get(DATA_CONTEXT_DI_PROVIDER)!;
|
||||
const connectionInfoPropertiesResource = di.getService(ConnectionInfoPropertiesResource);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
|
||||
return new ConnectionFormDriverPropertiesPart(formState, connectionInfoPropertiesResource, optionsPart);
|
||||
});
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { ConnectionInfoOrigin, ConnectionInfoOriginResource, ConnectionInfoResource, DatabaseConnection } from '@cloudbeaver/core-connections';
|
||||
import type { IExecutor, IExecutorHandlersCollection } from '@cloudbeaver/core-executor';
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
import type { IFormStateInfo } from '@cloudbeaver/core-ui';
|
||||
import type { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
export type ConnectionFormMode = 'edit' | 'create';
|
||||
export type ConnectionFormType = 'admin' | 'public';
|
||||
|
||||
export interface IConnectionFormState {
|
||||
mode: ConnectionFormMode;
|
||||
type: ConnectionFormType;
|
||||
projectId: string | null;
|
||||
|
||||
config: ConnectionConfig;
|
||||
|
||||
partsState: MetadataMap<string, any>;
|
||||
|
||||
readonly id: string;
|
||||
readonly initError: Error | null;
|
||||
readonly statusMessage: string | string[] | null;
|
||||
readonly disabled: boolean;
|
||||
readonly loading: boolean;
|
||||
readonly configured: boolean;
|
||||
|
||||
readonly availableDrivers: string[];
|
||||
readonly resource: ConnectionInfoResource;
|
||||
readonly originResource: ConnectionInfoOriginResource;
|
||||
readonly info: DatabaseConnection | undefined;
|
||||
readonly originInfo: ConnectionInfoOrigin | undefined;
|
||||
readonly readonly: boolean;
|
||||
readonly submittingTask: IExecutorHandlersCollection<IConnectionFormSubmitData>;
|
||||
readonly closeTask: IExecutor;
|
||||
|
||||
readonly load: () => Promise<void>;
|
||||
readonly loadConnectionInfo: () => Promise<DatabaseConnection | undefined>;
|
||||
readonly reset: () => void;
|
||||
readonly setPartsState: (state: MetadataMap<string, any>) => this;
|
||||
readonly setOptions: (mode: ConnectionFormMode, type: ConnectionFormType) => this;
|
||||
readonly setConfig: (projectId: string, config: ConnectionConfig) => this;
|
||||
readonly setProject: (projectId: string) => this;
|
||||
readonly setAvailableDrivers: (drivers: string[]) => this;
|
||||
readonly save: () => Promise<void>;
|
||||
readonly test: () => Promise<void>;
|
||||
readonly checkFormState: () => Promise<IFormStateInfo | null>;
|
||||
readonly close: () => Promise<void>;
|
||||
readonly dispose: () => void;
|
||||
}
|
||||
|
||||
export interface IConnectionFormProps {
|
||||
state: IConnectionFormState;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export interface IConnectionFormFillConfigData {
|
||||
updated: boolean;
|
||||
state: IConnectionFormState;
|
||||
}
|
||||
|
||||
export interface IConnectionFormSubmitData {
|
||||
submitType: 'submit' | 'test';
|
||||
state: IConnectionFormState;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { schema } from '@cloudbeaver/core-utils';
|
||||
import type { IFormProps } from '@cloudbeaver/core-ui';
|
||||
|
||||
export const CONNECTION_FORM_STATE_SCHEMA = schema
|
||||
.object({
|
||||
projectId: schema.string(),
|
||||
availableDrivers: schema.array(schema.string()),
|
||||
requiredNetworkHandlersIds: schema.array(schema.string()),
|
||||
connectionId: schema.string().or(schema.undefined()),
|
||||
type: schema.enum(['admin', 'public']),
|
||||
})
|
||||
.required()
|
||||
.strict();
|
||||
|
||||
export type IConnectionFormState = schema.infer<typeof CONNECTION_FORM_STATE_SCHEMA>;
|
||||
export type IConnectionFormProps = IFormProps<IConnectionFormState>;
|
||||
+565
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { FormMode, FormPart, formSubmitContext, formValidationContext, type IFormState } from '@cloudbeaver/core-ui';
|
||||
import { DriverConfigurationType, type ConnectionConfig, type ObjectPropertyInfo, type TestConnectionMutation } from '@cloudbeaver/core-sdk';
|
||||
import { Executor, ExecutorInterrupter, type IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor';
|
||||
import {
|
||||
ConnectionInfoAuthPropertiesResource,
|
||||
ConnectionInfoCustomOptionsResource,
|
||||
ConnectionInfoProjectKey,
|
||||
ConnectionInfoProviderPropertiesResource,
|
||||
ConnectionInfoResource,
|
||||
createConnectionParam,
|
||||
DatabaseAuthModelsResource,
|
||||
DBDriverResource,
|
||||
type ConnectionInfoAuthProperties,
|
||||
type DBDriver,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import type { ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { action, computed, makeObservable, observable, reaction, toJS } from 'mobx';
|
||||
import { getUniqueName, isNotNullDefined } from '@cloudbeaver/core-utils';
|
||||
import { getDefaultConfigurationType } from './getDefaultConfigurationType.js';
|
||||
import { getConnectionName } from './getConnectionName.js';
|
||||
import type { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import type { IConnectionFormOptionsState } from './IConnectionFormOptionsState.js';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { ConnectionAuthenticationDialogLoader } from '../../ConnectionAuthentication/ConnectionAuthenticationDialogLoader.js';
|
||||
import type { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { parseJdbcUri } from '@dbeaver/jdbc-uri-parser';
|
||||
|
||||
const MAIN_PROPERTY_DATABASE_KEY = 'database';
|
||||
const MAIN_PROPERTY_HOST_KEY = 'host';
|
||||
const MAIN_PROPERTY_PORT_KEY = 'port';
|
||||
const MAIN_PROPERTY_SERVER_KEY = 'server';
|
||||
|
||||
const defaultStateGetter = (connectionId?: string) =>
|
||||
({
|
||||
connectionId,
|
||||
configurationType: DriverConfigurationType.Manual,
|
||||
keepAliveInterval: 0,
|
||||
credentials: {},
|
||||
template: false,
|
||||
mainPropertyValues: {},
|
||||
networkHandlersConfig: [],
|
||||
properties: {},
|
||||
providerProperties: {},
|
||||
}) as IConnectionFormOptionsState;
|
||||
|
||||
export class ConnectionFormOptionsPart extends FormPart<IConnectionFormOptionsState, IConnectionFormState> {
|
||||
private disposeReaction: () => void;
|
||||
|
||||
readonly onDriverIdChange: IExecutor<string | undefined>;
|
||||
|
||||
constructor(
|
||||
formState: IFormState<IConnectionFormState>,
|
||||
private readonly dbDriverResource: DBDriverResource,
|
||||
private readonly projectInfoResource: ProjectInfoResource,
|
||||
private readonly databaseAuthModelsResource: DatabaseAuthModelsResource,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
private readonly connectionInfoAuthPropertiesResource: ConnectionInfoAuthPropertiesResource,
|
||||
private readonly connectionInfoCustomOptionsResource: ConnectionInfoCustomOptionsResource,
|
||||
private readonly connectionInfoProviderPropertiesResource: ConnectionInfoProviderPropertiesResource,
|
||||
private readonly localizationService: LocalizationService,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
private readonly notificationService: NotificationService,
|
||||
) {
|
||||
super(formState, defaultStateGetter(formState.state.connectionId));
|
||||
|
||||
this.onDriverIdChange = new Executor();
|
||||
|
||||
this.disposeReaction = reaction(
|
||||
() => this.getNameTemplate(),
|
||||
(value, prev) => {
|
||||
if (this.formState.mode === 'edit') {
|
||||
return;
|
||||
}
|
||||
if (!this.state.name || prev === this.state.name) {
|
||||
this.state.name = value;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
makeObservable<this, 'askCredentials'>(this, {
|
||||
setAuthModelId: action.bound,
|
||||
setDriverId: action.bound,
|
||||
connectionKey: computed,
|
||||
askCredentials: action.bound,
|
||||
});
|
||||
}
|
||||
|
||||
private async askCredentials(state: IConnectionFormOptionsState): Promise<IConnectionFormOptionsState | null> {
|
||||
const driver = this.state.driverId ? this.dbDriverResource.get(this.state.driverId) : undefined;
|
||||
const isCredentialsRequired = !state.saveCredentials || this.formState.state.requiredNetworkHandlersIds.length;
|
||||
|
||||
if (!isCredentialsRequired || driver?.anonymousAccess) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const stateToRestore: IConnectionFormOptionsState = {};
|
||||
|
||||
// does not show credentials in dialog if they are already saved
|
||||
if (state.saveCredentials) {
|
||||
if (state.authModelId) {
|
||||
stateToRestore.authModelId = state.authModelId;
|
||||
delete state.authModelId;
|
||||
}
|
||||
|
||||
if (state.credentials) {
|
||||
stateToRestore.credentials = toJS(state.credentials);
|
||||
delete state.credentials;
|
||||
}
|
||||
}
|
||||
|
||||
// does not show network handlers in dialog if they are not required
|
||||
if (!this.formState.state.requiredNetworkHandlersIds.length && state.networkHandlersConfig) {
|
||||
stateToRestore.networkHandlersConfig = toJS(state.networkHandlersConfig);
|
||||
delete state.networkHandlersConfig;
|
||||
}
|
||||
|
||||
const result = await this.commonDialogService.open(ConnectionAuthenticationDialogLoader, {
|
||||
config: state,
|
||||
authModelId: state.authModelId ?? null,
|
||||
networkHandlers: this.formState.state.requiredNetworkHandlersIds,
|
||||
projectId: this.formState.state.projectId,
|
||||
});
|
||||
|
||||
if (result === DialogueStateResult.Rejected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(stateToRestore)) {
|
||||
state[key as keyof IConnectionFormOptionsState] = observable(stateToRestore[key as keyof IConnectionFormOptionsState] as any);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
get connectionKey() {
|
||||
if (!this.initialState.connectionId || !this.formState.state.projectId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createConnectionParam(this.formState.state.projectId, this.initialState.connectionId);
|
||||
}
|
||||
|
||||
// do not check outdated of userInfoResource cause it synced with projectInfoResource which is handled in optionsPart outdated method
|
||||
// otherwise you would get an infinite loading of the form
|
||||
override isOutdated(): boolean {
|
||||
if (!this.formState.state.projectId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.formState.mode === 'create') {
|
||||
if (this.state.driverId && this.dbDriverResource.isOutdated(this.state.driverId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.connectionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAuthPropertiesOutdated = this.connectionInfoAuthPropertiesResource.isOutdated(this.connectionKey);
|
||||
const isCustomOptionsOutdated = this.connectionInfoCustomOptionsResource.isOutdated(this.connectionKey);
|
||||
const isProviderPropertiesOutdated = this.connectionInfoProviderPropertiesResource.isOutdated(this.connectionKey);
|
||||
|
||||
return isAuthPropertiesOutdated || isCustomOptionsOutdated || isProviderPropertiesOutdated;
|
||||
}
|
||||
|
||||
protected override async loader(): Promise<void> {
|
||||
if (this.formState.mode === 'create') {
|
||||
this.setInitialState(defaultStateGetter(this.initialState.connectionId ?? this.formState.state.connectionId));
|
||||
|
||||
await this.setDriverId(this.state.driverId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.connectionKey) {
|
||||
console.error('Connection connection key should be defined');
|
||||
return;
|
||||
}
|
||||
|
||||
const [authPropertiesInfo, customOptionsInfo, providerPropertiesInfo] = await Promise.all([
|
||||
this.connectionInfoAuthPropertiesResource.load(this.connectionKey),
|
||||
this.connectionInfoCustomOptionsResource.load(this.connectionKey),
|
||||
this.connectionInfoProviderPropertiesResource.load(this.connectionKey),
|
||||
]);
|
||||
|
||||
const config: ConnectionConfig = defaultStateGetter();
|
||||
|
||||
config.connectionId = customOptionsInfo.id;
|
||||
config.configurationType = customOptionsInfo.configurationType;
|
||||
|
||||
config.name = customOptionsInfo.name;
|
||||
config.description = customOptionsInfo.description;
|
||||
config.template = customOptionsInfo.template;
|
||||
config.driverId = customOptionsInfo.driverId;
|
||||
|
||||
config.host = customOptionsInfo.host || customOptionsInfo.mainPropertyValues?.[MAIN_PROPERTY_HOST_KEY];
|
||||
config.port = customOptionsInfo.port || customOptionsInfo.mainPropertyValues?.[MAIN_PROPERTY_PORT_KEY];
|
||||
config.serverName = customOptionsInfo.serverName || customOptionsInfo.mainPropertyValues?.[MAIN_PROPERTY_SERVER_KEY];
|
||||
config.databaseName = customOptionsInfo.databaseName || customOptionsInfo.mainPropertyValues?.[MAIN_PROPERTY_DATABASE_KEY];
|
||||
|
||||
config.url = customOptionsInfo.url;
|
||||
config.folder = customOptionsInfo.folder;
|
||||
|
||||
config.authModelId = authPropertiesInfo.authModel;
|
||||
config.saveCredentials = authPropertiesInfo.credentialsSaved;
|
||||
config.sharedCredentials = authPropertiesInfo.sharedCredentials;
|
||||
|
||||
config.keepAliveInterval = customOptionsInfo.keepAliveInterval;
|
||||
config.autocommit = customOptionsInfo.autocommit;
|
||||
config.readOnly = customOptionsInfo.readOnly;
|
||||
|
||||
if (authPropertiesInfo.authProperties) {
|
||||
for (const property of authPropertiesInfo.authProperties) {
|
||||
if (!property.features.includes('password')) {
|
||||
config.credentials[property.id!] = property.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (providerPropertiesInfo.providerProperties) {
|
||||
config.providerProperties = { ...toJS(providerPropertiesInfo.providerProperties) };
|
||||
}
|
||||
|
||||
if (customOptionsInfo.mainPropertyValues) {
|
||||
config.mainPropertyValues = { ...toJS(customOptionsInfo.mainPropertyValues) };
|
||||
}
|
||||
|
||||
this.formState.state.availableDrivers = [customOptionsInfo.driverId];
|
||||
|
||||
this.setInitialState(config);
|
||||
}
|
||||
|
||||
private getNameTemplate() {
|
||||
const driver = this.state.driverId ? this.dbDriverResource.get(this.state.driverId) : undefined;
|
||||
|
||||
if (this.state.configurationType === DriverConfigurationType.Url) {
|
||||
const parsedJdbcUri = parseJdbcUri(this.state.url ?? '');
|
||||
return getConnectionName({
|
||||
driverName: driver?.name || 'New connection',
|
||||
host: parsedJdbcUri.host,
|
||||
port: parsedJdbcUri.port,
|
||||
defaultPort: driver?.defaultPort,
|
||||
});
|
||||
}
|
||||
|
||||
return getConnectionName({
|
||||
driverName: driver?.name || 'New connection',
|
||||
host: this.state.host,
|
||||
port: this.state.port,
|
||||
defaultPort: driver?.defaultPort,
|
||||
});
|
||||
}
|
||||
|
||||
async setDriverId(driverId: string | undefined): Promise<void> {
|
||||
if (this.formState.mode === 'edit') {
|
||||
return;
|
||||
}
|
||||
|
||||
let driver: DBDriver | undefined;
|
||||
let prevDriver: DBDriver | undefined;
|
||||
const prevDriverId = this.state.driverId;
|
||||
this.state.driverId = driverId;
|
||||
|
||||
if (driverId) {
|
||||
driver = await this.dbDriverResource.load(driverId, ['includeProviderProperties']);
|
||||
}
|
||||
|
||||
if (!driver) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevDriverId) {
|
||||
prevDriver = await this.dbDriverResource.load(prevDriverId, ['includeProviderProperties']);
|
||||
}
|
||||
|
||||
if (!this.state.configurationType || !driver?.configurationTypes.includes(this.state.configurationType)) {
|
||||
this.state.configurationType = getDefaultConfigurationType(driver);
|
||||
}
|
||||
|
||||
if ((!prevDriver && this.state.host === undefined) || this.state.host === prevDriver?.defaultServer) {
|
||||
this.state.host = driver?.defaultServer || 'localhost';
|
||||
}
|
||||
|
||||
if ((!prevDriver && this.state.port === undefined) || this.state.port === prevDriver?.defaultPort) {
|
||||
this.state.port = driver?.defaultPort;
|
||||
}
|
||||
|
||||
if ((!prevDriver && this.state.databaseName === undefined) || this.state.databaseName === prevDriver?.defaultDatabase) {
|
||||
this.state.databaseName = driver?.defaultDatabase;
|
||||
}
|
||||
|
||||
if ((!prevDriver && this.state.url === undefined) || this.state.url === prevDriver?.sampleURL) {
|
||||
this.state.url = driver?.sampleURL;
|
||||
}
|
||||
|
||||
if (driver?.id !== prevDriver?.id) {
|
||||
this.state.credentials = {};
|
||||
this.state.providerProperties = {};
|
||||
this.setAuthModelId(driver?.defaultAuthModel);
|
||||
}
|
||||
|
||||
await this.onDriverIdChange.execute(this.state.driverId);
|
||||
}
|
||||
|
||||
setAuthModelId(modelId: string | undefined): void {
|
||||
if (modelId === this.initialState.authModelId) {
|
||||
this.state.credentials = { ...this.initialState.credentials };
|
||||
} else if (modelId !== this.state.authModelId) {
|
||||
this.state.credentials = {};
|
||||
}
|
||||
|
||||
this.state.authModelId = modelId;
|
||||
}
|
||||
|
||||
protected override async format(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {
|
||||
if (!this.state.driverId || !this.formState.state.projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const driver = await this.dbDriverResource.load(this.state.driverId, ['includeProviderProperties', 'includeMainProperties']);
|
||||
|
||||
this.formState.state.requiredNetworkHandlersIds = observable([]);
|
||||
this.state.networkHandlersConfig = observable([]);
|
||||
this.state.keepAliveInterval = this.state.keepAliveInterval ? Number(this.state.keepAliveInterval) : undefined;
|
||||
|
||||
this.state.name = this.state.name?.trim();
|
||||
this.state.description = this.state.description?.trim();
|
||||
|
||||
if (this.state.template || !this.state.folder) {
|
||||
delete this.state.folder;
|
||||
}
|
||||
|
||||
if (this.state.configurationType === DriverConfigurationType.Url) {
|
||||
this.state.url = this.state.url?.trim();
|
||||
} else {
|
||||
// if manual type configuration set, it helps to keep host, port, etc. properties (not saved on backend)
|
||||
delete this.state.url;
|
||||
}
|
||||
|
||||
// databaseName, host, port, serverName only saves on backend like this
|
||||
if (this.state.configurationType === DriverConfigurationType.Manual && !driver.useCustomPage) {
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_DATABASE_KEY] = this.state.databaseName?.trim();
|
||||
|
||||
if (!driver.embedded) {
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_HOST_KEY] = this.state.host?.trim();
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_PORT_KEY] = this.state.port?.trim();
|
||||
}
|
||||
|
||||
if (driver.requiresServerName) {
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_SERVER_KEY] = this.state.serverName?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if ((this.state.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) {
|
||||
this.state.authModelId = this.state.authModelId || driver.defaultAuthModel;
|
||||
this.state.saveCredentials = this.state.saveCredentials || this.state.sharedCredentials;
|
||||
const authPropertiesInfo = this.connectionKey ? await this.connectionInfoAuthPropertiesResource.load(this.connectionKey) : undefined;
|
||||
|
||||
const properties = await this.getConnectionAuthModelProperties(this.state.authModelId, authPropertiesInfo);
|
||||
const passwordProperty = properties.find(property => property.features.includes('password'));
|
||||
const isPasswordEmpty =
|
||||
passwordProperty &&
|
||||
(this.state.credentials?.[passwordProperty.id!] === passwordProperty.defaultValue || !this.state.credentials?.[passwordProperty.id!]);
|
||||
|
||||
if (isCredentialsChanged(properties, this.state.credentials!)) {
|
||||
this.state.credentials = prepareDynamicProperties(properties, toJS(this.state.credentials!));
|
||||
}
|
||||
|
||||
if (isPasswordEmpty) {
|
||||
delete this.state.credentials?.[passwordProperty.id!];
|
||||
}
|
||||
}
|
||||
|
||||
if (driver.providerProperties.length > 0) {
|
||||
this.state.providerProperties = prepareDynamicProperties(
|
||||
driver.providerProperties,
|
||||
toJS(this.state.providerProperties!),
|
||||
this.state.configurationType,
|
||||
);
|
||||
}
|
||||
|
||||
if (driver.useCustomPage && driver.mainProperties.length > 0) {
|
||||
this.state.mainPropertyValues = prepareDynamicProperties(driver.mainProperties, this.state.mainPropertyValues!, this.state.configurationType);
|
||||
}
|
||||
}
|
||||
|
||||
private async getConnectionAuthModelProperties(authModelId: string, connectionInfo?: ConnectionInfoAuthProperties): Promise<ObjectPropertyInfo[]> {
|
||||
const authModel = await this.databaseAuthModelsResource.load(authModelId);
|
||||
|
||||
let properties = authModel.properties;
|
||||
|
||||
if (connectionInfo?.authProperties && connectionInfo.authProperties.length > 0) {
|
||||
properties = connectionInfo.authProperties;
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private getTestMessageInfo(testContext: TestConnectionMutation['connection']) {
|
||||
let message = '';
|
||||
|
||||
if (testContext.clientVersion) {
|
||||
message += this.localizationService.translate('plugin_connections_connection_client_version', undefined, {
|
||||
version: testContext.clientVersion,
|
||||
});
|
||||
}
|
||||
|
||||
if (testContext.serverVersion) {
|
||||
message += this.localizationService.translate('plugin_connections_connection_server_version', undefined, {
|
||||
version: testContext.serverVersion,
|
||||
});
|
||||
}
|
||||
|
||||
if (testContext.connectTime) {
|
||||
message += this.localizationService.translate('plugin_connections_connection_connection_time', undefined, {
|
||||
time: testContext.connectTime,
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
protected override async validate(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {
|
||||
const validation = contexts.getContext(formValidationContext);
|
||||
|
||||
if (this.state.configurationType === DriverConfigurationType.Manual && this.state.host?.length === 0 && this.state.driverId) {
|
||||
const driver = await this.dbDriverResource.load(this.state.driverId);
|
||||
if (!driver.embedded) {
|
||||
validation.error('plugin_connections_connection_form_host_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.state.name?.length) {
|
||||
validation.error('plugin_connections_connection_form_name_invalid');
|
||||
}
|
||||
|
||||
if (this.state.driverId && this.state.configurationType) {
|
||||
const driver = await this.dbDriverResource.load(this.state.driverId, ['includeProviderProperties']);
|
||||
|
||||
if (!driver.configurationTypes.includes(this.state.configurationType)) {
|
||||
validation.error('plugin_connections_connection_form_host_configuration_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.formState.state.projectId !== null && this.formState.mode === 'create') {
|
||||
const project = this.projectInfoResource.get(this.formState.state.projectId);
|
||||
|
||||
if (!project?.canEditDataSources) {
|
||||
validation.error('plugin_connections_connection_form_project_invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async saveChanges(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {
|
||||
if (!this.formState.state.projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const submitType = contexts.getContext(formSubmitContext);
|
||||
if (submitType.type === 'submit') {
|
||||
if (this.formState.mode === 'edit') {
|
||||
await this.connectionInfoResource.update(this.connectionKey!, this.state);
|
||||
} else {
|
||||
const connections = await this.connectionInfoResource.load(ConnectionInfoProjectKey(this.formState.state.projectId));
|
||||
const connectionNames = connections.map(connection => connection.name);
|
||||
|
||||
const uniqueName = getUniqueName(this.state.name || '', connectionNames);
|
||||
const connection = await this.connectionInfoResource.create(this.formState.state.projectId, { ...this.state, name: uniqueName });
|
||||
this.state.connectionId = connection.id;
|
||||
this.initialState.connectionId = connection.id;
|
||||
this.formState.setMode(FormMode.Edit);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const stateCopy = await this.askCredentials(observable(toJS(this.state)));
|
||||
|
||||
if (!stateCopy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = await this.connectionInfoResource.test(this.formState.state.projectId, stateCopy);
|
||||
|
||||
this.notificationService.logSuccess({
|
||||
title: 'plugin_connections_connection_established',
|
||||
message: this.getTestMessageInfo(info),
|
||||
});
|
||||
} catch (error) {
|
||||
this.notificationService.logException(error as Error, 'connections_connection_test_fail');
|
||||
} finally {
|
||||
// to prevent form from resetting the state after saving
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.disposeReaction();
|
||||
}
|
||||
}
|
||||
|
||||
function prepareDynamicProperties(
|
||||
propertiesInfo: ObjectPropertyInfo[],
|
||||
properties: Record<string, any>,
|
||||
configurationType?: DriverConfigurationType,
|
||||
) {
|
||||
const result: Record<string, any> = { ...properties };
|
||||
|
||||
for (const propertyInfo of propertiesInfo) {
|
||||
if (!propertyInfo.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const supported = configurationType === undefined || propertyInfo.supportedConfigurationTypes?.some(type => type === configurationType);
|
||||
|
||||
if (!supported) {
|
||||
delete result[propertyInfo.id];
|
||||
} else {
|
||||
const isDefault = isNotNullDefined(propertyInfo.defaultValue);
|
||||
if (!(propertyInfo.id in result) && isDefault) {
|
||||
result[propertyInfo.id] = propertyInfo.defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(result)) {
|
||||
if (typeof result[key] === 'string') {
|
||||
result[key] = result[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isCredentialsChanged(authProperties: ObjectPropertyInfo[], credentials: Record<string, any>) {
|
||||
for (const property of authProperties) {
|
||||
const value = credentials[property.id!];
|
||||
|
||||
if (property.features.includes('password')) {
|
||||
if (value !== undefined) {
|
||||
return property.features.includes('file') ? true : !!value;
|
||||
}
|
||||
} else if (value !== property.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
+2
-432
@@ -5,459 +5,29 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, makeObservable, runInAction, toJS } from 'mobx';
|
||||
import React from 'react';
|
||||
|
||||
import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import {
|
||||
ConnectionInfoProjectKey,
|
||||
createConnectionParam,
|
||||
DatabaseAuthModelsResource,
|
||||
type DatabaseConnection,
|
||||
DBDriverResource,
|
||||
isJDBCConnection,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import { ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { DriverConfigurationType, isObjectPropertyInfoStateEqual, type ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
|
||||
import { formStateContext } from '@cloudbeaver/core-ui';
|
||||
import { getUniqueName, isNotNullDefined, isValuesEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js';
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import { connectionConfigContext } from '../Contexts/connectionConfigContext.js';
|
||||
import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext.js';
|
||||
import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps.js';
|
||||
import { getConnectionName } from './getConnectionName.js';
|
||||
import { getDefaultConfigurationType } from './getDefaultConfigurationType.js';
|
||||
|
||||
export const Options = React.lazy(async () => {
|
||||
const { Options } = await import('./Options.js');
|
||||
return { default: Options };
|
||||
});
|
||||
|
||||
const MAIN_PROPERTY_DATABASE_KEY = 'database';
|
||||
const MAIN_PROPERTY_HOST_KEY = 'host';
|
||||
const MAIN_PROPERTY_PORT_KEY = 'port';
|
||||
const MAIN_PROPERTY_SERVER_KEY = 'server';
|
||||
|
||||
@injectable()
|
||||
export class ConnectionOptionsTabService extends Bootstrap {
|
||||
constructor(
|
||||
private readonly projectInfoResource: ProjectInfoResource,
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly dbDriverResource: DBDriverResource,
|
||||
private readonly userInfoResource: UserInfoResource,
|
||||
private readonly localizationService: LocalizationService,
|
||||
private readonly authProvidersResource: AuthProvidersResource,
|
||||
private readonly databaseAuthModelsResource: DatabaseAuthModelsResource,
|
||||
) {
|
||||
constructor(private readonly connectionFormService: ConnectionFormService) {
|
||||
super();
|
||||
|
||||
makeObservable<this, 'fillConfig'>(this, {
|
||||
fillConfig: action,
|
||||
});
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.tabsContainer.add({
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'options',
|
||||
name: 'plugin_connections_connection_form_part_main',
|
||||
order: 1,
|
||||
panel: () => Options,
|
||||
});
|
||||
|
||||
this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this));
|
||||
|
||||
this.connectionFormService.formValidationTask.addHandler(this.validate.bind(this));
|
||||
|
||||
this.connectionFormService.formSubmittingTask.addHandler(this.save.bind(this));
|
||||
|
||||
this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)).addHandler(this.formAuthState.bind(this));
|
||||
|
||||
this.connectionFormService.configureTask.addHandler(this.configure.bind(this));
|
||||
|
||||
this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this));
|
||||
}
|
||||
|
||||
private async save({ state, submitType }: IConnectionFormSubmitData, contexts: IExecutionContextProvider<IConnectionFormSubmitData>) {
|
||||
const status = contexts.getContext(this.connectionFormService.connectionStatusContext);
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
|
||||
if (!state.projectId) {
|
||||
status.error('connections_connection_create_fail');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (submitType === 'submit') {
|
||||
if (state.mode === 'edit') {
|
||||
const connection = await state.resource.update(createConnectionParam(state.projectId, config.connectionId!), config);
|
||||
status.info('Connection was updated');
|
||||
status.info(connection.name);
|
||||
} else {
|
||||
const connection = await state.resource.create(state.projectId, config);
|
||||
config.connectionId = connection.id;
|
||||
status.info('Connection was created');
|
||||
status.info(connection.name);
|
||||
}
|
||||
} else {
|
||||
const info = await state.resource.test(state.projectId, config);
|
||||
status.info('Connection is established');
|
||||
status.info('Client version: ' + info.clientVersion);
|
||||
status.info('Server version: ' + info.serverVersion);
|
||||
status.info('Connection time: ' + info.connectTime);
|
||||
}
|
||||
} catch (exception: any) {
|
||||
if (submitType === 'submit') {
|
||||
status.error('connections_connection_create_fail', exception);
|
||||
} else {
|
||||
status.error('connections_connection_test_fail', exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async validate({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider<IConnectionFormSubmitData>) {
|
||||
const validation = contexts.getContext(this.connectionFormService.connectionValidationContext);
|
||||
|
||||
if (state.config.configurationType === DriverConfigurationType.Manual && state.config.host?.length === 0 && state.config.driverId) {
|
||||
const driver = await this.dbDriverResource.load(state.config.driverId);
|
||||
if (!driver.embedded) {
|
||||
validation.error('plugin_connections_connection_form_host_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.config.name?.length) {
|
||||
validation.error('plugin_connections_connection_form_name_invalid');
|
||||
}
|
||||
|
||||
if (state.config.driverId && state.config.configurationType) {
|
||||
const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']);
|
||||
|
||||
if (!driver.configurationTypes.includes(state.config.configurationType)) {
|
||||
validation.error('plugin_connections_connection_form_host_configuration_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
if (state.projectId !== null && state.mode === 'create') {
|
||||
const project = this.projectInfoResource.get(state.projectId);
|
||||
|
||||
if (!project?.canEditDataSources) {
|
||||
validation.error('plugin_connections_connection_form_project_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
// if (state.config.folder && !state.config.folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
|
||||
// validation.error('connections_connection_folder_validation');
|
||||
// }
|
||||
}
|
||||
|
||||
private async setDefaults(state: IConnectionFormState) {
|
||||
if (!state.config.driverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']);
|
||||
|
||||
state.config.authModelId = driver?.defaultAuthModel;
|
||||
state.config.configurationType = getDefaultConfigurationType(driver);
|
||||
|
||||
if (!state.config.host) {
|
||||
state.config.host = driver?.defaultServer || 'localhost';
|
||||
}
|
||||
|
||||
if (!state.config.port) {
|
||||
state.config.port = driver?.defaultPort;
|
||||
}
|
||||
|
||||
state.config.databaseName = driver?.defaultDatabase;
|
||||
state.config.url = driver?.sampleURL;
|
||||
|
||||
if (isJDBCConnection(driver)) {
|
||||
state.config.name = state.config.url;
|
||||
} else {
|
||||
state.config.name = getConnectionName(driver.name || '', state.config.host, state.config.port, driver.defaultPort);
|
||||
}
|
||||
}
|
||||
|
||||
private async fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider<IConnectionFormFillConfigData>) {
|
||||
if (!updated) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.config.credentials || updated) {
|
||||
state.config.credentials = {};
|
||||
state.config.saveCredentials = false;
|
||||
}
|
||||
|
||||
if (!state.config.providerProperties || updated) {
|
||||
state.config.providerProperties = {};
|
||||
}
|
||||
|
||||
if (!state.config.mainPropertyValues || updated) {
|
||||
state.config.mainPropertyValues = {};
|
||||
}
|
||||
|
||||
if (!state.info) {
|
||||
await this.setDefaults(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.config.connectionId = state.info.id;
|
||||
state.config.configurationType = state.info.configurationType;
|
||||
|
||||
state.config.name = state.info.name;
|
||||
state.config.description = state.info.description;
|
||||
state.config.template = state.info.template;
|
||||
state.config.driverId = state.info.driverId;
|
||||
|
||||
state.config.host = state.info.mainPropertyValues[MAIN_PROPERTY_HOST_KEY];
|
||||
state.config.port = state.info.mainPropertyValues[MAIN_PROPERTY_PORT_KEY];
|
||||
state.config.serverName = state.info.mainPropertyValues[MAIN_PROPERTY_SERVER_KEY];
|
||||
state.config.databaseName = state.info.mainPropertyValues[MAIN_PROPERTY_DATABASE_KEY];
|
||||
|
||||
state.config.url = state.info.url;
|
||||
state.config.folder = state.info.folder;
|
||||
|
||||
state.config.authModelId = state.info.authModel;
|
||||
state.config.saveCredentials = state.info.credentialsSaved;
|
||||
state.config.sharedCredentials = state.info.sharedCredentials;
|
||||
|
||||
state.config.keepAliveInterval = state.info.keepAliveInterval;
|
||||
state.config.autocommit = state.info.autocommit;
|
||||
state.config.readOnly = state.info.readOnly;
|
||||
|
||||
if (state.info.authProperties) {
|
||||
for (const property of state.info.authProperties) {
|
||||
if (!property.features.includes('password')) {
|
||||
state.config.credentials[property.id!] = property.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.info.providerProperties) {
|
||||
state.config.providerProperties = { ...state.info.providerProperties };
|
||||
}
|
||||
|
||||
if (state.info.mainPropertyValues) {
|
||||
state.config.mainPropertyValues = { ...state.info.mainPropertyValues };
|
||||
}
|
||||
}
|
||||
|
||||
private configure(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const configuration = contexts.getContext(connectionFormConfigureContext);
|
||||
|
||||
configuration.include('includeAuthProperties', 'includeCredentialsSaved', 'customIncludeOptions');
|
||||
}
|
||||
|
||||
private async prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider<IConnectionFormSubmitData>) {
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
const credentialsState = contexts.getContext(connectionCredentialsStateContext);
|
||||
|
||||
if (!state.config.driverId || !state.projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties', 'includeMainProperties']);
|
||||
const tempConfig = toJS(config);
|
||||
|
||||
if (state.mode === 'edit') {
|
||||
tempConfig.connectionId = state.config.connectionId;
|
||||
}
|
||||
|
||||
tempConfig.configurationType = state.config.configurationType;
|
||||
tempConfig.name = state.config.name?.trim();
|
||||
|
||||
if (tempConfig.name && state.mode === 'create') {
|
||||
const connections = await state.resource.load(ConnectionInfoProjectKey(state.projectId));
|
||||
const connectionNames = connections.map(connection => connection.name);
|
||||
|
||||
tempConfig.name = getUniqueName(tempConfig.name, connectionNames);
|
||||
}
|
||||
|
||||
tempConfig.description = state.config.description?.trim();
|
||||
tempConfig.template = state.config.template;
|
||||
tempConfig.driverId = state.config.driverId;
|
||||
tempConfig.keepAliveInterval = Number(state.config.keepAliveInterval);
|
||||
tempConfig.autocommit = state.config.autocommit;
|
||||
tempConfig.readOnly = state.config.readOnly;
|
||||
|
||||
if (!state.config.template && state.config.folder) {
|
||||
tempConfig.folder = state.config.folder;
|
||||
}
|
||||
|
||||
if (tempConfig.configurationType === DriverConfigurationType.Url) {
|
||||
tempConfig.url = state.config.url?.trim();
|
||||
}
|
||||
|
||||
tempConfig.mainPropertyValues = toJS(state.config.mainPropertyValues);
|
||||
|
||||
if (tempConfig.configurationType === DriverConfigurationType.Manual && !driver.useCustomPage) {
|
||||
tempConfig.mainPropertyValues[MAIN_PROPERTY_DATABASE_KEY] = state.config.databaseName?.trim();
|
||||
|
||||
if (!driver.embedded) {
|
||||
tempConfig.mainPropertyValues[MAIN_PROPERTY_HOST_KEY] = state.config.host?.trim();
|
||||
tempConfig.mainPropertyValues[MAIN_PROPERTY_PORT_KEY] = state.config.port?.trim();
|
||||
}
|
||||
|
||||
if (driver.requiresServerName) {
|
||||
tempConfig.mainPropertyValues[MAIN_PROPERTY_SERVER_KEY] = state.config.serverName?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if ((state.config.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) {
|
||||
tempConfig.authModelId = state.config.authModelId || driver.defaultAuthModel;
|
||||
tempConfig.sharedCredentials = state.config.sharedCredentials;
|
||||
tempConfig.saveCredentials = state.config.saveCredentials || tempConfig.sharedCredentials;
|
||||
|
||||
const properties = await this.getConnectionAuthModelProperties(tempConfig.authModelId, state.info);
|
||||
|
||||
if (this.isCredentialsChanged(properties, state.config.credentials)) {
|
||||
tempConfig.credentials = this.prepareDynamicProperties(properties, toJS(state.config.credentials));
|
||||
}
|
||||
|
||||
if (!tempConfig.saveCredentials) {
|
||||
credentialsState.requireAuthModel(tempConfig.authModelId);
|
||||
}
|
||||
}
|
||||
|
||||
if (driver.providerProperties.length > 0) {
|
||||
tempConfig.providerProperties = this.prepareDynamicProperties(
|
||||
driver.providerProperties,
|
||||
toJS(state.config.providerProperties),
|
||||
tempConfig.configurationType,
|
||||
);
|
||||
}
|
||||
|
||||
if (driver.useCustomPage && driver.mainProperties.length > 0) {
|
||||
tempConfig.mainPropertyValues = this.prepareDynamicProperties(
|
||||
driver.mainProperties,
|
||||
tempConfig.mainPropertyValues,
|
||||
tempConfig.configurationType,
|
||||
);
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.assign(config, tempConfig);
|
||||
});
|
||||
}
|
||||
|
||||
private prepareDynamicProperties(
|
||||
propertiesInfo: ObjectPropertyInfo[],
|
||||
properties: Record<string, any>,
|
||||
configurationType?: DriverConfigurationType,
|
||||
) {
|
||||
const result: Record<string, any> = { ...properties };
|
||||
|
||||
for (const propertyInfo of propertiesInfo) {
|
||||
if (!propertyInfo.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const supported = configurationType === undefined || propertyInfo.supportedConfigurationTypes?.some(type => type === configurationType);
|
||||
|
||||
if (!supported) {
|
||||
delete result[propertyInfo.id];
|
||||
} else {
|
||||
const isDefault = isNotNullDefined(propertyInfo.defaultValue);
|
||||
if (!(propertyInfo.id in result) && isDefault) {
|
||||
result[propertyInfo.id] = propertyInfo.defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(result)) {
|
||||
if (typeof result[key] === 'string') {
|
||||
result[key] = result[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async formAuthState(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
const stateContext = contexts.getContext(formStateContext);
|
||||
|
||||
const driver = await this.dbDriverResource.load(config.driverId!, ['includeProviderProperties', 'includeMainProperties']);
|
||||
const authModel = await this.databaseAuthModelsResource.load(config.authModelId ?? data.info?.authModel ?? driver.defaultAuthModel);
|
||||
|
||||
const providerId = authModel.requiredAuth ?? data.info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID;
|
||||
|
||||
await this.userInfoResource.load();
|
||||
|
||||
if (!this.userInfoResource.hasToken(providerId)) {
|
||||
const provider = await this.authProvidersResource.load(providerId);
|
||||
const message = this.localizationService.translate('plugin_connections_connection_cloud_auth_required', undefined, {
|
||||
providerLabel: provider.label,
|
||||
});
|
||||
stateContext.setInfo(message);
|
||||
stateContext.readonly = data.mode === 'edit';
|
||||
}
|
||||
}
|
||||
|
||||
private async formState(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
if (!data.info) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
const stateContext = contexts.getContext(formStateContext);
|
||||
const driver = await this.dbDriverResource.load(data.config.driverId!, ['includeProviderProperties', 'includeMainProperties']);
|
||||
|
||||
if (
|
||||
!isValuesEqual(config.name, data.info.name, '') ||
|
||||
!isValuesEqual(config.configurationType, data.info.configurationType, DriverConfigurationType.Manual) ||
|
||||
!isValuesEqual(config.description, data.info.description, '') ||
|
||||
!isValuesEqual(config.template, data.info.template, true) ||
|
||||
!isValuesEqual(config.folder, data.info.folder, undefined) ||
|
||||
!isValuesEqual(config.driverId, data.info.driverId, '') ||
|
||||
(config.url !== undefined && !isValuesEqual(config.url, data.info.url, '')) ||
|
||||
(config.host !== undefined && !isValuesEqual(config.host, data.info.host, '')) ||
|
||||
(config.port !== undefined && !isValuesEqual(config.port, data.info.port, '')) ||
|
||||
(config.serverName !== undefined && !isValuesEqual(config.serverName, data.info.serverName, '')) ||
|
||||
(config.databaseName !== undefined && !isValuesEqual(config.databaseName, data.info.databaseName, '')) ||
|
||||
config.credentials !== undefined ||
|
||||
(config.authModelId !== undefined && !isValuesEqual(config.authModelId, data.info.authModel, '')) ||
|
||||
(config.saveCredentials !== undefined && config.saveCredentials !== data.info.credentialsSaved) ||
|
||||
(config.sharedCredentials !== undefined && config.sharedCredentials !== data.info.sharedCredentials) ||
|
||||
(config.providerProperties !== undefined &&
|
||||
!isObjectPropertyInfoStateEqual(driver.providerProperties, config.providerProperties, data.info.providerProperties)) ||
|
||||
(config.mainPropertyValues !== undefined &&
|
||||
!isObjectPropertyInfoStateEqual(driver.mainProperties, config.mainPropertyValues, data.info.mainPropertyValues)) ||
|
||||
(config.keepAliveInterval !== undefined && !isValuesEqual(config.keepAliveInterval, data.info.keepAliveInterval)) ||
|
||||
(config.autocommit !== undefined && !isValuesEqual(config.autocommit, data.info.autocommit))
|
||||
) {
|
||||
stateContext.markEdited();
|
||||
}
|
||||
}
|
||||
|
||||
private isCredentialsChanged(authProperties: ObjectPropertyInfo[], credentials: Record<string, any>) {
|
||||
for (const property of authProperties) {
|
||||
const value = credentials[property.id!];
|
||||
|
||||
if (property.features.includes('password')) {
|
||||
if (value !== undefined) {
|
||||
return property.features.includes('file') ? true : !!value;
|
||||
}
|
||||
} else if (value !== property.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async getConnectionAuthModelProperties(authModelId: string, connectionInfo?: DatabaseConnection): Promise<ObjectPropertyInfo[]> {
|
||||
const authModel = await this.databaseAuthModelsResource.load(authModelId);
|
||||
|
||||
let properties = authModel.properties;
|
||||
|
||||
if (connectionInfo?.authProperties && connectionInfo.authProperties.length > 0) {
|
||||
properties = connectionInfo.authProperties;
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { DriverConfigurationType } from '@cloudbeaver/core-sdk';
|
||||
import { schema } from '@cloudbeaver/core-utils';
|
||||
import { CONNECTION_NETWORK_HANDLER_SCHEMA } from './IConnectionNetworkHanler.js';
|
||||
|
||||
export const CONNECTION_PROPERTIES_SCHEMA = schema.record(schema.any());
|
||||
|
||||
export const CONNECTION_CONFIG_SCHEMA = schema.object({
|
||||
authModelId: schema.string().optional(),
|
||||
autocommit: schema.boolean().optional(),
|
||||
configurationType: schema.enum([DriverConfigurationType.Manual, DriverConfigurationType.Url]).optional(),
|
||||
connectionId: schema.string().optional(),
|
||||
credentials: schema.record(schema.any()).optional(),
|
||||
dataSourceId: schema.string().optional(),
|
||||
databaseName: schema.string().optional(),
|
||||
description: schema.string().optional(),
|
||||
driverId: schema.string().optional(),
|
||||
folder: schema.string().optional(),
|
||||
host: schema.string().optional(),
|
||||
keepAliveInterval: schema.number().optional(),
|
||||
mainPropertyValues: schema.record(schema.any()).optional(),
|
||||
name: schema.string().optional(),
|
||||
networkHandlersConfig: schema.array(CONNECTION_NETWORK_HANDLER_SCHEMA).optional(),
|
||||
port: schema.string().optional(),
|
||||
properties: CONNECTION_PROPERTIES_SCHEMA.optional(),
|
||||
providerProperties: schema.record(schema.any()).optional(),
|
||||
readOnly: schema.boolean().optional(),
|
||||
saveCredentials: schema.boolean().optional(),
|
||||
selectedSecretId: schema.string().optional(),
|
||||
serverName: schema.string().optional(),
|
||||
sharedCredentials: schema.boolean().optional(),
|
||||
template: schema.boolean().optional(),
|
||||
templateId: schema.string().optional(),
|
||||
url: schema.string().optional(),
|
||||
userName: schema.string().optional(),
|
||||
userPassword: schema.string().optional(),
|
||||
});
|
||||
|
||||
export type IConnectionProperties = schema.infer<typeof CONNECTION_PROPERTIES_SCHEMA>;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { schema } from '@cloudbeaver/core-utils';
|
||||
import { CONNECTION_CONFIG_SCHEMA } from './IConnectionConfig.js';
|
||||
|
||||
export const CONNECTION_FORM_OPTIONS_SCHEMA = CONNECTION_CONFIG_SCHEMA.extend({});
|
||||
|
||||
export type IConnectionFormOptionsState = schema.infer<typeof CONNECTION_FORM_OPTIONS_SCHEMA>;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { NetworkHandlerAuthType } from '@cloudbeaver/core-sdk';
|
||||
import { schema } from '@cloudbeaver/core-utils';
|
||||
|
||||
export const CONNECTION_NETWORK_HANDLER_SCHEMA = schema.object({
|
||||
id: schema.string(),
|
||||
authType: schema.nativeEnum(NetworkHandlerAuthType).optional(),
|
||||
enabled: schema.boolean().optional(),
|
||||
key: schema.string().optional(),
|
||||
password: schema.string().optional(),
|
||||
properties: schema.record(schema.any()).optional(),
|
||||
savePassword: schema.boolean().optional(),
|
||||
secureProperties: schema.record(schema.any()).optional(),
|
||||
userName: schema.string().optional(),
|
||||
});
|
||||
|
||||
export type INetworkHandlerConfig = schema.infer<typeof CONNECTION_NETWORK_HANDLER_SCHEMA>;
|
||||
@@ -34,26 +34,34 @@ import {
|
||||
useS,
|
||||
useTranslate,
|
||||
useAuthenticationAction,
|
||||
useAutoLoad,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { DatabaseAuthModelsResource, type DBDriver, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections';
|
||||
import {
|
||||
ConnectionInfoAuthPropertiesResource,
|
||||
ConnectionInfoOriginResource,
|
||||
ConnectionInfoResource,
|
||||
DatabaseAuthModelsResource,
|
||||
type DBDriver,
|
||||
DBDriverResource,
|
||||
isLocalConnection,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { EAdminPermission, ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { DriverConfigurationType } from '@cloudbeaver/core-sdk';
|
||||
import { type TabContainerPanelComponent, TabsContext } from '@cloudbeaver/core-ui';
|
||||
import { type TabContainerPanelComponent, TabsContext, useTab } from '@cloudbeaver/core-ui';
|
||||
import { EMPTY_ARRAY } from '@cloudbeaver/core-utils';
|
||||
import { ProjectSelect } from '@cloudbeaver/plugin-projects';
|
||||
|
||||
import { ConnectionAuthModelCredentialsForm } from '../ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.js';
|
||||
import { ConnectionAuthModelSelector } from '../ConnectionAuthModelCredentials/ConnectionAuthModelSelector.js';
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormProps.js';
|
||||
import { CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID } from '../SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.js';
|
||||
import { AdvancedPropertiesForm } from './AdvancedPropertiesForm.js';
|
||||
import styles from './Options.module.css';
|
||||
import { ParametersForm } from './ParametersForm.js';
|
||||
import { ProviderPropertiesForm } from './ProviderPropertiesForm.js';
|
||||
import { useOptions } from './useOptions.js';
|
||||
import { getConnectionFormOptionsPart } from './getConnectionFormOptionsPart.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
|
||||
const PROFILE_AUTH_MODEL_ID = 'profile';
|
||||
|
||||
@@ -77,107 +85,112 @@ const driverConfiguration: IDriverConfiguration[] = [
|
||||
isVisible: driver => driver.configurationTypes.includes(DriverConfigurationType.Url),
|
||||
},
|
||||
];
|
||||
|
||||
export const Options: TabContainerPanelComponent<IConnectionFormProps> = observer(function Options({ state }) {
|
||||
export const Options: TabContainerPanelComponent<IConnectionFormProps> = observer(function Options({ formState, tabId }) {
|
||||
const { selected } = useTab(tabId);
|
||||
const isAdmin = usePermission(EAdminPermission.admin);
|
||||
const serverConfigResource = useResource(Options, ServerConfigResource, undefined);
|
||||
const serverConfigResource = useResource(Options, ServerConfigResource, undefined, {
|
||||
active: selected,
|
||||
});
|
||||
const projectInfoResource = useService(ProjectInfoResource);
|
||||
const service = useService(ConnectionFormService);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const translate = useTranslate();
|
||||
const { info, originInfo, config, availableDrivers, submittingTask: submittingHandlers, disabled } = state;
|
||||
const style = useS(styles);
|
||||
const tabsState = useContext(TabsContext);
|
||||
const isSharedProject = projectInfoResource.isProjectShared(state.projectId);
|
||||
const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const connectionInfoResource = useResource(Options, ConnectionInfoResource, optionsPart.connectionKey, {
|
||||
active: selected && !!optionsPart.connectionKey,
|
||||
});
|
||||
const connectionInfoOriginResource = useResource(Options, ConnectionInfoOriginResource, optionsPart.connectionKey, {
|
||||
active: selected && !!optionsPart.connectionKey,
|
||||
});
|
||||
const connectionInfoAuthPropertiesResource = useResource(Options, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey, {
|
||||
active: selected && !!optionsPart.connectionKey,
|
||||
});
|
||||
|
||||
//@TODO it's here until the profile implementation in the CloudBeaver
|
||||
const readonly = state.readonly || info?.authModel === PROFILE_AUTH_MODEL_ID;
|
||||
const readonly = formState.isDisabled || formState.isReadOnly || connectionInfoResource.data?.authModel === PROFILE_AUTH_MODEL_ID;
|
||||
|
||||
useFormValidator(submittingHandlers.for(service.formValidationTask), formRef.current);
|
||||
const optionsHook = useOptions(state);
|
||||
useFormValidator(formState.validationTask, formRef.current);
|
||||
const { credentialsSavingEnabled } = useAdministrationSettings();
|
||||
|
||||
const driverMap = useResource(
|
||||
Options,
|
||||
DBDriverResource,
|
||||
{ key: config.driverId || null, includes: ['includeProviderProperties', 'includeMainProperties'] as const },
|
||||
{
|
||||
onData: data => {
|
||||
optionsHook.setDefaults(data);
|
||||
},
|
||||
key: optionsPart.state.driverId || null,
|
||||
includes: ['includeProviderProperties', 'includeMainProperties', 'includeDriverProperties'] as const,
|
||||
},
|
||||
{
|
||||
active: selected,
|
||||
},
|
||||
);
|
||||
|
||||
const driver = driverMap.data;
|
||||
const configurationTypes = driverConfiguration.filter(configuration => driver && configuration.isVisible(driver));
|
||||
|
||||
function handleFormChange(value?: unknown, name?: string) {
|
||||
if (name !== 'name' && optionsHook.isNameAutoFill()) {
|
||||
optionsHook.updateNameTemplate(driver);
|
||||
}
|
||||
|
||||
if (config.template) {
|
||||
config.folder = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const applicableAuthModels = driver?.applicableAuthModels ?? [];
|
||||
|
||||
const authModelLoader = useResource(
|
||||
Options,
|
||||
DatabaseAuthModelsResource,
|
||||
getComputed(() => config.authModelId || info?.authModel || driver?.defaultAuthModel || null),
|
||||
getComputed(() => optionsPart.state.authModelId || connectionInfoResource.data?.authModel || driver?.defaultAuthModel || null),
|
||||
{
|
||||
onData: data => optionsHook.setAuthModel(data),
|
||||
active: selected,
|
||||
},
|
||||
);
|
||||
|
||||
const authModel = authModelLoader.data;
|
||||
|
||||
async function handleAuthModelSelect(id: string | undefined) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = await authModelLoader.resource.load(id);
|
||||
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
optionsHook.setAuthModel(model);
|
||||
function handleAuthModelSelect(authModelId: string | undefined) {
|
||||
optionsPart.setAuthModelId(authModelId);
|
||||
}
|
||||
|
||||
const authentication = useAuthenticationAction({
|
||||
providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID,
|
||||
providerId: authModel?.requiredAuth ?? connectionInfoResource.data?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID,
|
||||
});
|
||||
|
||||
const edit = state.mode === 'edit';
|
||||
const originLocal = !info || (originInfo?.origin && isLocalConnection(originInfo.origin));
|
||||
const edit = formState.mode === 'edit';
|
||||
const originLocal =
|
||||
!connectionInfoResource.data || (connectionInfoOriginResource.data?.origin && isLocalConnection(connectionInfoOriginResource.data.origin));
|
||||
|
||||
const drivers = driverMap.resource.enabledDrivers.filter(({ id, driverInstalled }) => {
|
||||
if (!edit && !isAdmin && !driverInstalled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return availableDrivers.includes(id);
|
||||
return formState.state.availableDrivers.includes(id);
|
||||
});
|
||||
|
||||
function setProject(projectId: string) {
|
||||
formState.state.projectId = projectId;
|
||||
}
|
||||
|
||||
let properties = authModel?.properties;
|
||||
|
||||
if (info?.authProperties && info.authProperties.length > 0 && config.authModelId === info.authModel) {
|
||||
properties = info.authProperties;
|
||||
if (
|
||||
connectionInfoAuthPropertiesResource.data?.authProperties &&
|
||||
connectionInfoAuthPropertiesResource.data.authProperties.length > 0 &&
|
||||
optionsPart.state.authModelId === connectionInfoResource.data?.authModel
|
||||
) {
|
||||
properties = connectionInfoAuthPropertiesResource.data.authProperties;
|
||||
}
|
||||
|
||||
const sharedCredentials = config.sharedCredentials && serverConfigResource.data?.distributed;
|
||||
const sharedCredentials = optionsPart.state.sharedCredentials && serverConfigResource.data?.distributed;
|
||||
|
||||
function openCredentialsTab(event: React.MouseEvent<HTMLAnchorElement>) {
|
||||
event.preventDefault();
|
||||
tabsState?.open(CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID);
|
||||
}
|
||||
|
||||
async function setDriverIdHandler(driverId: string | undefined) {
|
||||
await optionsPart.setDriverId(driverId);
|
||||
}
|
||||
|
||||
useAutoLoad(Options, optionsPart, selected);
|
||||
|
||||
return (
|
||||
<Form ref={formRef} className={s(style, { form: true })} disabled={driverMap.isLoading()} onChange={handleFormChange}>
|
||||
<Form ref={formRef} className={s(style, { form: true })} disabled={driverMap.isLoading()}>
|
||||
<ColoredContainer wrap overflow parent gap>
|
||||
<Container medium gap>
|
||||
<Group gap>
|
||||
@@ -191,7 +204,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
<Container wrap gap>
|
||||
<Combobox
|
||||
name="driverId"
|
||||
state={config}
|
||||
value={optionsPart.state.driverId}
|
||||
items={drivers}
|
||||
keySelector={driver => driver.id}
|
||||
valueSelector={driver => driver.name ?? ''}
|
||||
@@ -199,17 +212,18 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
iconSelector={driver => driver.icon}
|
||||
searchable={drivers.length > 10}
|
||||
readOnly={readonly || edit || drivers.length < 2}
|
||||
disabled={disabled}
|
||||
disabled={formState.isDisabled}
|
||||
loading={driverMap.isLoading()}
|
||||
tiny
|
||||
fill
|
||||
onSelect={setDriverIdHandler}
|
||||
>
|
||||
{translate('connections_connection_driver')}
|
||||
</Combobox>
|
||||
{configurationTypes.length > 1 && (
|
||||
<FormFieldDescription label={translate('connections_connection_configuration')} tiny>
|
||||
<Container gap>
|
||||
<RadioGroup name="configurationType" state={config}>
|
||||
<RadioGroup name="configurationType" state={optionsPart.state}>
|
||||
{configurationTypes.map(conf => (
|
||||
<Radio
|
||||
key={conf.value}
|
||||
@@ -228,32 +242,32 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
</FormFieldDescription>
|
||||
)}
|
||||
</Container>
|
||||
{config.configurationType === DriverConfigurationType.Url && (
|
||||
{optionsPart.state.configurationType === DriverConfigurationType.Url && (
|
||||
<InputField
|
||||
type="text"
|
||||
name="url"
|
||||
state={config}
|
||||
readOnly={readonly || disabled}
|
||||
autoComplete={`section-${config.driverId || 'driver'} section-jdbc`}
|
||||
state={optionsPart.state}
|
||||
readOnly={readonly || formState.isDisabled}
|
||||
autoComplete={`section-${optionsPart.state.driverId || 'driver'} section-jdbc`}
|
||||
>
|
||||
{translate('plugin_connections_connection_form_part_main_url_jdbc')}
|
||||
</InputField>
|
||||
)}
|
||||
|
||||
{config.configurationType === DriverConfigurationType.Manual &&
|
||||
{optionsPart.state.configurationType === DriverConfigurationType.Manual &&
|
||||
(driver?.useCustomPage ? (
|
||||
<ObjectPropertyInfoForm
|
||||
state={config.mainPropertyValues}
|
||||
state={optionsPart.state.mainPropertyValues}
|
||||
properties={driver.mainProperties ?? EMPTY_ARRAY}
|
||||
disabled={disabled}
|
||||
disabled={formState.isDisabled}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
) : (
|
||||
<ParametersForm
|
||||
config={config}
|
||||
config={optionsPart.state}
|
||||
embedded={driver?.embedded}
|
||||
requiresServerName={driver?.requiresServerName}
|
||||
disabled={disabled}
|
||||
disabled={formState.isDisabled}
|
||||
readOnly={readonly}
|
||||
originLocal={originLocal}
|
||||
/>
|
||||
@@ -262,24 +276,24 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
</Group>
|
||||
<Group form gap>
|
||||
<Container wrap gap>
|
||||
<InputField type="text" name="name" minLength={1} state={config} readOnly={readonly || disabled} required fill>
|
||||
<InputField type="text" name="name" minLength={1} state={optionsPart.state} readOnly={readonly || formState.isDisabled} required fill>
|
||||
{translate('connections_connection_name')}
|
||||
</InputField>
|
||||
{!config.template && (
|
||||
{!optionsPart.state.template && (
|
||||
<ProjectSelect
|
||||
value={state.projectId}
|
||||
value={formState.state.projectId}
|
||||
readOnly={readonly || edit}
|
||||
disabled={disabled}
|
||||
disabled={formState.isDisabled}
|
||||
autoHide
|
||||
onChange={projectId => state.setProject(projectId)}
|
||||
onChange={setProject}
|
||||
/>
|
||||
)}
|
||||
{!config.template && (
|
||||
{!optionsPart.state.template && (
|
||||
<InputField
|
||||
type="text"
|
||||
name="folder"
|
||||
state={config}
|
||||
autoComplete={`section-${config.driverId || 'driver'} section-folder`}
|
||||
state={optionsPart.state}
|
||||
autoComplete={`section-${optionsPart.state.driverId || 'driver'} section-folder`}
|
||||
autoHide
|
||||
readOnly
|
||||
tiny
|
||||
@@ -289,7 +303,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
</InputField>
|
||||
)}
|
||||
</Container>
|
||||
<Textarea name="description" rows={3} state={config} readOnly={readonly || disabled}>
|
||||
<Textarea name="description" rows={3} state={optionsPart.state} readOnly={readonly || formState.isDisabled}>
|
||||
{translate('connections_connection_description')}
|
||||
</Textarea>
|
||||
</Group>
|
||||
@@ -300,32 +314,32 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
<GroupTitle>{translate('connections_connection_edit_authentication')}</GroupTitle>
|
||||
{serverConfigResource.resource.distributed && isSharedProject && (
|
||||
<FieldCheckbox
|
||||
id={config.connectionId + 'isShared'}
|
||||
id={optionsPart.state.connectionId + 'isShared'}
|
||||
name="sharedCredentials"
|
||||
title={translate('connections_connection_share_credentials_tooltip')}
|
||||
state={config}
|
||||
disabled={disabled || readonly}
|
||||
state={optionsPart.state}
|
||||
disabled={formState.isDisabled || readonly}
|
||||
keepSize
|
||||
>
|
||||
{translate('connections_connection_share_credentials')}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
<ConnectionAuthModelSelector
|
||||
authModelCredentialsState={config}
|
||||
authModelCredentialsState={optionsPart.state}
|
||||
applicableAuthModels={applicableAuthModels}
|
||||
readonlyAuthModelId={!originLocal}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
disabled={formState.isDisabled}
|
||||
onAuthModelChange={handleAuthModelSelect}
|
||||
/>
|
||||
{!sharedCredentials ? (
|
||||
<>
|
||||
{properties && (
|
||||
<ConnectionAuthModelCredentialsForm
|
||||
credentials={config.credentials}
|
||||
credentials={optionsPart.state.credentials}
|
||||
properties={properties}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
disabled={formState.isDisabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -337,12 +351,12 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
</Link>
|
||||
</FormFieldDescription>
|
||||
)}
|
||||
{!sharedCredentials && authModel && credentialsSavingEnabled && !config.template && (
|
||||
{!sharedCredentials && authModel && credentialsSavingEnabled && !optionsPart.state.template && (
|
||||
<FieldCheckbox
|
||||
id={config.connectionId + 'authNeeded'}
|
||||
id={optionsPart.state.connectionId + 'authNeeded'}
|
||||
name="saveCredentials"
|
||||
state={config}
|
||||
disabled={disabled || readonly || config.sharedCredentials}
|
||||
state={optionsPart.state}
|
||||
disabled={formState.isDisabled || readonly || optionsPart.state.sharedCredentials}
|
||||
mod={['primary']}
|
||||
title={translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
@@ -361,10 +375,15 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
</Group>
|
||||
)}
|
||||
{driver?.providerProperties && (
|
||||
<ProviderPropertiesForm config={config} properties={driver.providerProperties} disabled={disabled} readonly={readonly} />
|
||||
<ProviderPropertiesForm
|
||||
config={optionsPart.state}
|
||||
properties={driver.providerProperties}
|
||||
disabled={formState.isDisabled}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AdvancedPropertiesForm config={config} disabled={disabled} readonly={readonly} />
|
||||
<AdvancedPropertiesForm config={optionsPart.state} disabled={formState.isDisabled} readonly={readonly} />
|
||||
</Container>
|
||||
</ColoredContainer>
|
||||
</Form>
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context';
|
||||
import type { IFormState } from '@cloudbeaver/core-ui';
|
||||
import { ConnectionFormOptionsPart } from './ConnectionFormOptionsPart.js';
|
||||
import {
|
||||
ConnectionInfoAuthPropertiesResource,
|
||||
ConnectionInfoCustomOptionsResource,
|
||||
ConnectionInfoProviderPropertiesResource,
|
||||
ConnectionInfoResource,
|
||||
DatabaseAuthModelsResource,
|
||||
DBDriverResource,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
|
||||
const DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART = createDataContext<ConnectionFormOptionsPart>('Connection Form Options Part');
|
||||
|
||||
export function getConnectionFormOptionsPart(formState: IFormState<IConnectionFormState>): ConnectionFormOptionsPart {
|
||||
return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART, context => {
|
||||
const di = context.get(DATA_CONTEXT_DI_PROVIDER)!;
|
||||
const dbDriverResource = di.getService(DBDriverResource);
|
||||
const projectInfoResource = di.getService(ProjectInfoResource);
|
||||
const databaseAuthModelsResource = di.getService(DatabaseAuthModelsResource);
|
||||
const connectionInfoResource = di.getService(ConnectionInfoResource);
|
||||
const localizationService = di.getService(LocalizationService);
|
||||
const commonDialogService = di.getService(CommonDialogService);
|
||||
const notificationService = di.getService(NotificationService);
|
||||
const connectionInfoAuthPropertiesResource = di.getService(ConnectionInfoAuthPropertiesResource);
|
||||
const connectionInfoCustomOptionsResource = di.getService(ConnectionInfoCustomOptionsResource);
|
||||
const connectionInfoProviderPropertiesResource = di.getService(ConnectionInfoProviderPropertiesResource);
|
||||
|
||||
return new ConnectionFormOptionsPart(
|
||||
formState,
|
||||
dbDriverResource,
|
||||
projectInfoResource,
|
||||
databaseAuthModelsResource,
|
||||
connectionInfoResource,
|
||||
connectionInfoAuthPropertiesResource,
|
||||
connectionInfoCustomOptionsResource,
|
||||
connectionInfoProviderPropertiesResource,
|
||||
localizationService,
|
||||
commonDialogService,
|
||||
notificationService,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
const MAX_HOST_LENGTH = 20;
|
||||
|
||||
export function getConnectionName(driverName: string, host?: string, port?: string, defaultPort?: string) {
|
||||
interface IConnectionNameOptions {
|
||||
driverName: string;
|
||||
host?: string;
|
||||
port?: string;
|
||||
defaultPort?: string;
|
||||
}
|
||||
|
||||
export function getConnectionName({ driverName, host, port, defaultPort }: IConnectionNameOptions): string {
|
||||
let name = driverName;
|
||||
|
||||
if (host) {
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { runInAction } from 'mobx';
|
||||
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { type DBDriver, DBDriverResource, isJDBCConnection } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import type { DatabaseAuthModel } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { IConnectionFormState } from '../IConnectionFormProps.js';
|
||||
import { getConnectionName } from './getConnectionName.js';
|
||||
import { getDefaultConfigurationType } from './getDefaultConfigurationType.js';
|
||||
|
||||
export function useOptions(state: IConnectionFormState) {
|
||||
const dbDriverResource = useService(DBDriverResource);
|
||||
const refObject = useObjectRef(
|
||||
() => ({
|
||||
prevName: null as string | null,
|
||||
prevDriverId: null as string | null,
|
||||
}),
|
||||
{
|
||||
state,
|
||||
},
|
||||
);
|
||||
|
||||
return useObjectRef({
|
||||
updateNameTemplate(driver: DBDriver | undefined) {
|
||||
runInAction(() => {
|
||||
const {
|
||||
state: { config, info },
|
||||
} = refObject;
|
||||
|
||||
if (isJDBCConnection(driver, info)) {
|
||||
refObject.prevName = config.url || '';
|
||||
config.name = config.url || '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!driver) {
|
||||
config.name = 'New connection';
|
||||
return;
|
||||
}
|
||||
|
||||
const name = getConnectionName(driver.name || '', config.host, config.port, driver.defaultPort);
|
||||
|
||||
refObject.prevName = name;
|
||||
config.name = name;
|
||||
});
|
||||
},
|
||||
setDefaults(driver: DBDriver | undefined) {
|
||||
runInAction(() => {
|
||||
const {
|
||||
state: { config, info },
|
||||
prevDriverId,
|
||||
} = refObject;
|
||||
|
||||
if (info || driver?.id !== config.driverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let prevDriver: DBDriver | undefined;
|
||||
|
||||
if (prevDriverId) {
|
||||
prevDriver = dbDriverResource.get(prevDriverId);
|
||||
}
|
||||
|
||||
refObject.prevDriverId = driver?.id || null;
|
||||
|
||||
if (!config.configurationType || !driver?.configurationTypes.includes(config.configurationType)) {
|
||||
state.config.configurationType = getDefaultConfigurationType(driver);
|
||||
}
|
||||
|
||||
if ((!prevDriver && config.host === undefined) || config.host === prevDriver?.defaultServer) {
|
||||
config.host = driver?.defaultServer || 'localhost';
|
||||
}
|
||||
|
||||
if ((!prevDriver && config.port === undefined) || config.port === prevDriver?.defaultPort) {
|
||||
config.port = driver?.defaultPort;
|
||||
}
|
||||
|
||||
if ((!prevDriver && config.databaseName === undefined) || config.databaseName === prevDriver?.defaultDatabase) {
|
||||
config.databaseName = driver?.defaultDatabase;
|
||||
}
|
||||
|
||||
if ((!prevDriver && config.url === undefined) || config.url === prevDriver?.sampleURL) {
|
||||
config.url = driver?.sampleURL;
|
||||
}
|
||||
|
||||
if (this.isNameAutoFill()) {
|
||||
this.updateNameTemplate(driver);
|
||||
}
|
||||
|
||||
if (driver?.id !== prevDriver?.id) {
|
||||
config.credentials = {};
|
||||
config.providerProperties = {};
|
||||
config.authModelId = driver?.defaultAuthModel;
|
||||
}
|
||||
});
|
||||
},
|
||||
setAuthModel(model: DatabaseAuthModel) {
|
||||
const {
|
||||
state: { config, info },
|
||||
} = refObject;
|
||||
|
||||
config.credentials = {};
|
||||
|
||||
if (model.id === info?.authModel) {
|
||||
if (info.authProperties) {
|
||||
for (const property of info.authProperties) {
|
||||
if (!property.features.includes('password')) {
|
||||
config.credentials[property.id!] = property.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refObject.state.checkFormState();
|
||||
},
|
||||
|
||||
isNameAutoFill() {
|
||||
const {
|
||||
prevName,
|
||||
state: { config, mode },
|
||||
} = refObject;
|
||||
|
||||
const isAutoFill = config.name === prevName || prevName === null;
|
||||
|
||||
return isAutoFill && mode === 'create';
|
||||
},
|
||||
});
|
||||
}
|
||||
+30
-16
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -9,24 +9,35 @@ import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
|
||||
import { Button, getComputed, type PlaceholderComponent, useResource, useTranslate, useAuthenticationAction } from '@cloudbeaver/core-blocks';
|
||||
import { DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { ConnectionInfoResource, DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
|
||||
import type { IConnectionFormProps } from '../IConnectionFormProps.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
export const AuthenticationButton: PlaceholderComponent<IConnectionFormProps> = observer(function ConnectionFormAuthenticationAction({ state }) {
|
||||
export const AuthenticationButton: PlaceholderComponent<IConnectionFormProps> = observer(function ConnectionFormAuthenticationAction({ formState }) {
|
||||
const translate = useTranslate();
|
||||
const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, state.config.driverId || null);
|
||||
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, optionsPart.state.driverId || null);
|
||||
const connectionInfoService = useResource(ConnectionFormAuthenticationAction, ConnectionInfoResource, optionsPart.connectionKey, {
|
||||
active: !!optionsPart.connectionKey,
|
||||
});
|
||||
const info = connectionInfoService.data;
|
||||
const driver = driverMap.data;
|
||||
const { data: authModel } = useResource(
|
||||
ConnectionFormAuthenticationAction,
|
||||
DatabaseAuthModelsResource,
|
||||
getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null),
|
||||
getComputed(() => optionsPart.state.authModelId || info?.authModel || driver?.defaultAuthModel || null),
|
||||
);
|
||||
|
||||
const authentication = useAuthenticationAction({
|
||||
providerId: authModel?.requiredAuth ?? state.info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID,
|
||||
onAuthenticate: () => state.loadConnectionInfo(),
|
||||
providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID,
|
||||
onAuthenticate: () => {
|
||||
if (!optionsPart.connectionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
connectionInfoService.resource.markOutdated(optionsPart.connectionKey);
|
||||
},
|
||||
});
|
||||
|
||||
if (authentication.authorized) {
|
||||
@@ -34,27 +45,30 @@ export const AuthenticationButton: PlaceholderComponent<IConnectionFormProps> =
|
||||
}
|
||||
|
||||
return (
|
||||
<Button type="button" disabled={state.disabled} mod={['outlined']} onClick={authentication.auth}>
|
||||
<Button type="button" disabled={formState.isDisabled} mod={['outlined']} onClick={authentication.auth}>
|
||||
{translate('authentication_authenticate')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
export const ConnectionFormAuthenticationAction: PlaceholderComponent<IConnectionFormProps> = observer(function ConnectionFormAuthenticationAction({
|
||||
state,
|
||||
formState,
|
||||
}) {
|
||||
const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, state.config.driverId || null);
|
||||
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, optionsPart.state.driverId || null);
|
||||
const connectionInfoService = useResource(ConnectionFormAuthenticationAction, ConnectionInfoResource, optionsPart.connectionKey, {
|
||||
active: !!optionsPart.connectionKey,
|
||||
});
|
||||
const driver = driverMap.data;
|
||||
const { data: authModel } = useResource(
|
||||
ConnectionFormAuthenticationAction,
|
||||
DatabaseAuthModelsResource,
|
||||
getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null),
|
||||
getComputed(() => optionsPart.state.authModelId || connectionInfoService.data?.authModel || driver?.defaultAuthModel || null),
|
||||
);
|
||||
|
||||
if (!authModel?.requiredAuth && !state.info?.requiredAuth) {
|
||||
if (!authModel?.requiredAuth && !connectionInfoService.data?.requiredAuth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <AuthenticationButton state={state} />;
|
||||
return <AuthenticationButton formState={formState} />;
|
||||
});
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { FormPart, formStateContext, type IFormState } from '@cloudbeaver/core-ui';
|
||||
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import type { IConnectionFormOriginInfoState } from './IConnectionFormOriginInfoState.js';
|
||||
import {
|
||||
ConnectionInfoAuthPropertiesResource,
|
||||
DatabaseAuthModelsResource,
|
||||
DBDriverResource,
|
||||
type ConnectionInfoOriginDetailsResource,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, type UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import { computed, makeObservable } from 'mobx';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import type { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import type { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js';
|
||||
|
||||
const defaultStateGetter = () => ({}) as IConnectionFormOriginInfoState;
|
||||
|
||||
@injectable()
|
||||
export class ConnectionFormOriginInfoFormPart extends FormPart<IConnectionFormOriginInfoState, IConnectionFormState> {
|
||||
constructor(
|
||||
formState: IFormState<IConnectionFormState>,
|
||||
private readonly connectionInfoOriginDetailsResource: ConnectionInfoOriginDetailsResource,
|
||||
private readonly userInfoResource: UserInfoResource,
|
||||
private readonly databaseAuthModelsResource: DatabaseAuthModelsResource,
|
||||
private readonly connectionInfoAuthPropertiesResource: ConnectionInfoAuthPropertiesResource,
|
||||
private readonly dbDriverResource: DBDriverResource,
|
||||
private readonly authProvidersResource: AuthProvidersResource,
|
||||
private readonly localizationService: LocalizationService,
|
||||
private readonly optionsPart: ConnectionFormOptionsPart,
|
||||
) {
|
||||
super(formState, defaultStateGetter());
|
||||
|
||||
makeObservable(this, {
|
||||
providerId: computed,
|
||||
isAuthenticated: computed,
|
||||
authModelId: computed,
|
||||
});
|
||||
|
||||
this.formState.formStateTask.addHandler(this.formAuthState.bind(this));
|
||||
}
|
||||
|
||||
override isOutdated(): boolean {
|
||||
if (!this.optionsPart.connectionKey || !this.optionsPart.state.driverId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.dbDriverResource.isOutdated(this.optionsPart.state.driverId) ||
|
||||
this.connectionInfoAuthPropertiesResource.isOutdated(this.optionsPart.connectionKey)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.authModelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.databaseAuthModelsResource.isOutdated(this.authModelId) || this.userInfoResource.isOutdated()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isAuthenticated && this.connectionInfoOriginDetailsResource.isOutdated(this.optionsPart.connectionKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get providerId(): string | null {
|
||||
if (!this.formState.state.projectId || !this.optionsPart.state.driverId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const driver = this.dbDriverResource.get(this.optionsPart.state.driverId);
|
||||
|
||||
if (!driver) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = this.optionsPart.connectionKey ? this.connectionInfoAuthPropertiesResource.get(this.optionsPart.connectionKey) : null;
|
||||
const authModel = this.databaseAuthModelsResource.get(this.optionsPart.state.authModelId ?? info?.authModel ?? driver?.defaultAuthModel ?? null);
|
||||
|
||||
return authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID;
|
||||
}
|
||||
|
||||
get isAuthenticated(): boolean {
|
||||
if (!this.providerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.userInfoResource.hasToken(this.providerId);
|
||||
}
|
||||
|
||||
get authModelId(): string | null {
|
||||
const driver = this.dbDriverResource.get(this.optionsPart.state.driverId!)!;
|
||||
const info = this.optionsPart.connectionKey ? this.connectionInfoAuthPropertiesResource.get(this.optionsPart.connectionKey) : null;
|
||||
|
||||
return this.optionsPart.state.authModelId ?? info?.authModel ?? driver?.defaultAuthModel ?? null;
|
||||
}
|
||||
|
||||
private async formAuthState(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const stateContext = contexts.getContext(formStateContext);
|
||||
|
||||
const info = this.optionsPart.connectionKey ? this.connectionInfoAuthPropertiesResource.get(this.optionsPart.connectionKey) : null;
|
||||
const driver = await this.dbDriverResource.load(this.optionsPart.state.driverId!, ['includeProviderProperties', 'includeMainProperties']);
|
||||
const [authModel] = await Promise.all([this.databaseAuthModelsResource.load(driver.defaultAuthModel), this.userInfoResource.load()]);
|
||||
const providerId = authModel.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID;
|
||||
|
||||
if (!this.userInfoResource.hasToken(providerId)) {
|
||||
const provider = await this.authProvidersResource.load(providerId);
|
||||
const message = this.localizationService.translate('plugin_connections_connection_cloud_auth_required', undefined, {
|
||||
providerLabel: provider.label,
|
||||
});
|
||||
stateContext.setInfo(message);
|
||||
stateContext.readonly = this.formState.mode === 'edit';
|
||||
}
|
||||
}
|
||||
|
||||
protected override async loader(): Promise<void> {
|
||||
const state = defaultStateGetter();
|
||||
|
||||
if (!this.optionsPart.connectionKey || !this.optionsPart.state.driverId) {
|
||||
this.setInitialState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.dbDriverResource.load(this.optionsPart.state.driverId),
|
||||
this.connectionInfoAuthPropertiesResource.load(this.optionsPart.connectionKey),
|
||||
]);
|
||||
|
||||
if (!this.authModelId) {
|
||||
throw new Error('Auth model is not defined');
|
||||
}
|
||||
|
||||
await Promise.all([this.databaseAuthModelsResource.load(this.authModelId), this.userInfoResource.load()]);
|
||||
|
||||
if (!this.isAuthenticated) {
|
||||
this.setInitialState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const originInfo = await this.connectionInfoOriginDetailsResource.load(this.optionsPart.connectionKey);
|
||||
|
||||
if (!originInfo.origin.details) {
|
||||
this.setInitialState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const property of originInfo.origin.details) {
|
||||
state[property.id!] = property.value;
|
||||
}
|
||||
|
||||
this.setInitialState(state);
|
||||
}
|
||||
|
||||
protected override async saveChanges(data: IFormState<IConnectionFormState>): Promise<void> {}
|
||||
}
|
||||
+27
-19
@@ -1,44 +1,52 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import { isLocalConnection } from '@cloudbeaver/core-connections';
|
||||
import { ConnectionInfoOriginResource, isLocalConnection } from '@cloudbeaver/core-connections';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
export const ConnectionFormAuthenticationAction = importLazyComponent(() =>
|
||||
import('./ConnectionFormAuthenticationAction.js').then(m => m.ConnectionFormAuthenticationAction),
|
||||
);
|
||||
|
||||
export const ConnectionFormAuthenticationAction = React.lazy(async () => {
|
||||
const { ConnectionFormAuthenticationAction } = await import('./ConnectionFormAuthenticationAction.js');
|
||||
return { default: ConnectionFormAuthenticationAction };
|
||||
});
|
||||
export const OriginInfo = React.lazy(async () => {
|
||||
const { OriginInfo } = await import('./OriginInfo.js');
|
||||
return { default: OriginInfo };
|
||||
});
|
||||
export const OriginInfoTab = React.lazy(async () => {
|
||||
const { OriginInfoTab } = await import('./OriginInfoTab.js');
|
||||
return { default: OriginInfoTab };
|
||||
});
|
||||
const OriginInfo = importLazyComponent(() => import('./OriginInfo.js').then(m => m.OriginInfo));
|
||||
const OriginInfoTab = importLazyComponent(() => import('./OriginInfoTab.js').then(m => m.OriginInfoTab));
|
||||
|
||||
@injectable()
|
||||
export class ConnectionOriginInfoTabService extends Bootstrap {
|
||||
constructor(private readonly connectionFormService: ConnectionFormService) {
|
||||
constructor(
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly connectionInfoOriginResource: ConnectionInfoOriginResource,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.tabsContainer.add({
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'origin',
|
||||
order: 3,
|
||||
tab: () => OriginInfoTab,
|
||||
panel: () => OriginInfo,
|
||||
stateGetter: () => () => ({}),
|
||||
isHidden: (tabId, props) => (props?.state.originInfo ? isLocalConnection(props.state.originInfo.origin) : true),
|
||||
getLoader: () => getCachedMapResourceLoaderState(this.connectionInfoOriginResource, () => CachedMapAllKey),
|
||||
isHidden: (tabId, props) => {
|
||||
const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null;
|
||||
const key = optionsPart?.connectionKey;
|
||||
const originInfo = key ? this.connectionInfoOriginResource.get(key) : null;
|
||||
|
||||
if (!originInfo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isLocalConnection(originInfo.origin);
|
||||
},
|
||||
});
|
||||
|
||||
this.connectionFormService.actionsContainer.add(ConnectionFormAuthenticationAction, 0);
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { schema } from '@cloudbeaver/core-utils';
|
||||
|
||||
const CONNECTION_FORM_ORIGIN_INFO_SCHEMA = schema.record(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
id: schema.string().optional(),
|
||||
required: schema.boolean(),
|
||||
displayName: schema.string().optional(),
|
||||
description: schema.string().optional(),
|
||||
category: schema.string().optional(),
|
||||
dataType: schema.string().optional(),
|
||||
defaultValue: schema.any().optional(),
|
||||
validValues: schema.array(schema.any()).optional(),
|
||||
value: schema.any().optional(),
|
||||
length: schema.string(),
|
||||
features: schema.array(schema.string()),
|
||||
order: schema.number(),
|
||||
}),
|
||||
);
|
||||
|
||||
export type IConnectionFormOriginInfoState = schema.infer<typeof CONNECTION_FORM_ORIGIN_INFO_SCHEMA>;
|
||||
@@ -1,80 +1,54 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 { runInAction } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import { AuthProvidersResource } from '@cloudbeaver/core-authentication';
|
||||
import {
|
||||
ColoredContainer,
|
||||
ExceptionMessage,
|
||||
getComputed,
|
||||
Group,
|
||||
Loader,
|
||||
ObjectPropertyInfoForm,
|
||||
s,
|
||||
TextPlaceholder,
|
||||
useAutoLoad,
|
||||
useResource,
|
||||
useS,
|
||||
useTranslate,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
ConnectionInfoOriginDetailsResource,
|
||||
createConnectionParam,
|
||||
DatabaseAuthModelsResource,
|
||||
DBDriverResource,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui';
|
||||
import { ConnectionInfoOriginDetailsResource, ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui';
|
||||
|
||||
import type { IConnectionFormProps } from '../IConnectionFormProps.js';
|
||||
import styles from './OriginInfo.module.css';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
|
||||
export const OriginInfo: TabContainerPanelComponent<IConnectionFormProps> = observer(function OriginInfo({
|
||||
tabId,
|
||||
state: { info, resource, config },
|
||||
}) {
|
||||
import { getConnectionFormOriginInfoFormPart } from './getConnectionFormOriginInfoFormPart.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
export const OriginInfo: TabContainerPanelComponent<IConnectionFormProps> = observer(function OriginInfo({ tabId, formState }) {
|
||||
const tab = useTab(tabId);
|
||||
const translate = useTranslate();
|
||||
const userInfoLoader = useResource(OriginInfo, UserInfoResource, undefined);
|
||||
const state = useTabState<Record<string, any>>();
|
||||
const originInfoPart = getConnectionFormOriginInfoFormPart(formState);
|
||||
const style = useS(styles);
|
||||
const driverLoader = useResource(OriginInfo, DBDriverResource, config.driverId ?? null);
|
||||
const authModeLoader = useResource(
|
||||
OriginInfo,
|
||||
DatabaseAuthModelsResource,
|
||||
config.authModelId ?? info?.authModel ?? driverLoader.data?.defaultAuthModel ?? null,
|
||||
);
|
||||
|
||||
const providerId = authModeLoader.data?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID;
|
||||
const isAuthenticated = userInfoLoader.resource.hasToken(providerId);
|
||||
const providerLoader = useResource(OriginInfo, AuthProvidersResource, providerId);
|
||||
const connectionId = tab.selected && info ? createConnectionParam(info.projectId, info.id) : null;
|
||||
|
||||
const connectionOriginDetailsResource = useResource(OriginInfo, ConnectionInfoOriginDetailsResource, connectionId, {
|
||||
active: isAuthenticated,
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const providerLoader = useResource(OriginInfo, AuthProvidersResource, originInfoPart.providerId, {
|
||||
active: tab.selected,
|
||||
});
|
||||
const connection = useResource(OriginInfo, resource, connectionId, {
|
||||
active: isAuthenticated,
|
||||
onData: connection => {
|
||||
runInAction(() => {
|
||||
if (!connectionOriginDetailsResource.data?.origin.details) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const property of Object.keys(state)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete state[property];
|
||||
}
|
||||
|
||||
for (const property of connectionOriginDetailsResource.data.origin.details) {
|
||||
state[property.id!] = property.value;
|
||||
}
|
||||
});
|
||||
},
|
||||
const isAuthenticated = getComputed(() => originInfoPart.isAuthenticated);
|
||||
const connectionOriginDetailsResource = useResource(OriginInfo, ConnectionInfoOriginDetailsResource, optionsPart.connectionKey, {
|
||||
active: tab.selected && isAuthenticated,
|
||||
});
|
||||
const connection = useResource(OriginInfo, ConnectionInfoResource, optionsPart.connectionKey, {
|
||||
active: tab.selected && isAuthenticated,
|
||||
});
|
||||
|
||||
useAutoLoad(OriginInfo, originInfoPart);
|
||||
|
||||
if (connection.isLoading()) {
|
||||
return (
|
||||
@@ -115,7 +89,13 @@ export const OriginInfo: TabContainerPanelComponent<IConnectionFormProps> = obse
|
||||
return (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent>
|
||||
<Group large gap>
|
||||
<ObjectPropertyInfoForm properties={connectionOriginDetailsResource.data?.origin.details} state={state} readOnly small autoHide />
|
||||
<ObjectPropertyInfoForm
|
||||
properties={connectionOriginDetailsResource.data?.origin.details}
|
||||
state={originInfoPart.state}
|
||||
readOnly
|
||||
small
|
||||
autoHide
|
||||
/>
|
||||
</Group>
|
||||
<Loader key="overlay" className={s(style, { loader: true })} loading={connection.isLoading()} overlay />
|
||||
</ColoredContainer>
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 { Translate } from '@cloudbeaver/core-blocks';
|
||||
import { Translate, useResource } from '@cloudbeaver/core-blocks';
|
||||
import { Tab, type TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui';
|
||||
|
||||
import type { IConnectionFormProps } from '../IConnectionFormProps.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
import { ConnectionInfoOriginResource } from '@cloudbeaver/core-connections';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
export const OriginInfoTab: TabContainerTabComponent<IConnectionFormProps> = observer(function OriginInfoTab({ formState, ...rest }) {
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const connectionInfoOriginResource = useResource(OriginInfoTab, ConnectionInfoOriginResource, optionsPart.connectionKey, {
|
||||
active: !!optionsPart.connectionKey,
|
||||
});
|
||||
|
||||
export const OriginInfoTab: TabContainerTabComponent<IConnectionFormProps> = observer(function OriginInfoTab({ state: { originInfo }, ...rest }) {
|
||||
return (
|
||||
<Tab {...rest}>
|
||||
<TabTitle>
|
||||
<Translate token={originInfo?.origin?.displayName || 'Origin'} />
|
||||
<Translate token={connectionInfoOriginResource.data?.origin?.displayName || 'Origin'} />
|
||||
</TabTitle>
|
||||
</Tab>
|
||||
);
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { type IFormState } from '@cloudbeaver/core-ui';
|
||||
|
||||
import {
|
||||
ConnectionInfoAuthPropertiesResource,
|
||||
ConnectionInfoOriginDetailsResource,
|
||||
DatabaseAuthModelsResource,
|
||||
DBDriverResource,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import { ConnectionFormOriginInfoFormPart } from './ConnectionFormOriginInfoFormPart.js';
|
||||
import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context';
|
||||
import { AuthProvidersResource, UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
const DATA_CONTEXT_CONNECTION_FORM_ORIGIN_INFO_FORM_PART = createDataContext<ConnectionFormOriginInfoFormPart>(
|
||||
'Connection Form Origin Info Form Part',
|
||||
);
|
||||
|
||||
export function getConnectionFormOriginInfoFormPart(formState: IFormState<IConnectionFormState>): ConnectionFormOriginInfoFormPart {
|
||||
return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_ORIGIN_INFO_FORM_PART, context => {
|
||||
const di = context.get(DATA_CONTEXT_DI_PROVIDER)!;
|
||||
const connectionInfoOriginDetailsResource = di.getService(ConnectionInfoOriginDetailsResource);
|
||||
const userInfoResource = di.getService(UserInfoResource);
|
||||
const databaseAuthModelsResource = di.getService(DatabaseAuthModelsResource);
|
||||
const connectionInfoAuthPropertiesResource = di.getService(ConnectionInfoAuthPropertiesResource);
|
||||
const dbDriverResource = di.getService(DBDriverResource);
|
||||
const authProvidersResource = di.getService(AuthProvidersResource);
|
||||
const localizationService = di.getService(LocalizationService);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
|
||||
return new ConnectionFormOriginInfoFormPart(
|
||||
formState,
|
||||
connectionInfoOriginDetailsResource,
|
||||
userInfoResource,
|
||||
databaseAuthModelsResource,
|
||||
connectionInfoAuthPropertiesResource,
|
||||
dbDriverResource,
|
||||
authProvidersResource,
|
||||
localizationService,
|
||||
optionsPart,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui';
|
||||
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { DriverConfigurationType, NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk';
|
||||
import { ConnectionInfoNetworkHandlersResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections';
|
||||
import { toJS } from 'mobx';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import type { INetworkHandlerConfig } from '../Options/IConnectionNetworkHanler.js';
|
||||
import { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js';
|
||||
|
||||
const getDefaultState = () =>
|
||||
({
|
||||
id: SSH_TUNNEL_ID,
|
||||
enabled: false,
|
||||
authType: NetworkHandlerAuthType.Password,
|
||||
// should initially undefined cause if it's empty string it counts as saved password
|
||||
password: undefined,
|
||||
savePassword: false,
|
||||
userName: '',
|
||||
// should initially undefined cause if it's empty string it counts as saved private key
|
||||
key: undefined,
|
||||
properties: {
|
||||
port: 22,
|
||||
host: '',
|
||||
aliveInterval: '0',
|
||||
sshConnectTimeout: '10000',
|
||||
},
|
||||
}) as INetworkHandlerConfig;
|
||||
|
||||
export class ConnectionFormSSHPart extends FormPart<INetworkHandlerConfig, IConnectionFormState> {
|
||||
constructor(
|
||||
formState: IFormState<IConnectionFormState>,
|
||||
private readonly connectionInfoNetworkHandlersResource: ConnectionInfoNetworkHandlersResource,
|
||||
private readonly optionsPart: ConnectionFormOptionsPart,
|
||||
) {
|
||||
super(formState, getDefaultState());
|
||||
}
|
||||
|
||||
override isOutdated(): boolean {
|
||||
if (!this.optionsPart.connectionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.connectionInfoNetworkHandlersResource.isOutdated(this.optionsPart.connectionKey);
|
||||
}
|
||||
|
||||
protected override async loader(): Promise<void> {
|
||||
if (!this.optionsPart.connectionKey) {
|
||||
this.setInitialState(getDefaultState());
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = await this.connectionInfoNetworkHandlersResource.load(this.optionsPart.connectionKey);
|
||||
const sshHandler = connection?.networkHandlersConfig?.find(h => h.id === SSH_TUNNEL_ID);
|
||||
|
||||
this.setInitialState(sshHandler ?? getDefaultState());
|
||||
}
|
||||
|
||||
protected override async saveChanges(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {}
|
||||
|
||||
protected override format(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): void | Promise<void> {
|
||||
const urlType = this.optionsPart.state.configurationType === DriverConfigurationType.Url;
|
||||
|
||||
if (urlType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordChanged = isPasswordChanged(this.state, this.initialState);
|
||||
const keyChanged = isKeyChanged(this.state, this.initialState);
|
||||
|
||||
let handlerConfig: NetworkHandlerConfigInput = {
|
||||
...this.state,
|
||||
savePassword: this.state.savePassword || this.optionsPart.state.sharedCredentials,
|
||||
key: this.state.authType === NetworkHandlerAuthType.PublicKey && keyChanged ? this.state.key : undefined,
|
||||
password: passwordChanged ? this.state.password : undefined,
|
||||
};
|
||||
|
||||
delete handlerConfig.secureProperties;
|
||||
|
||||
if (this.state.enabled && !this.state.savePassword) {
|
||||
this.formState.state.requiredNetworkHandlersIds.push(this.state.id);
|
||||
} else if (!this.state.enabled) {
|
||||
this.formState.state.requiredNetworkHandlersIds = this.formState.state.requiredNetworkHandlersIds.filter(id => id !== this.state.id);
|
||||
}
|
||||
|
||||
if (handlerConfig) {
|
||||
handlerConfig = getTrimmedSSHConfig(handlerConfig);
|
||||
this.optionsPart.state.networkHandlersConfig!.push(handlerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
protected override validate(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): void | Promise<void> {
|
||||
const validation = contexts.getContext(formValidationContext);
|
||||
|
||||
if (!this.isChanged || !this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.savePassword && !this.state.userName?.length) {
|
||||
validation.error("Field SSH 'User' can't be empty");
|
||||
}
|
||||
|
||||
if (!this.state.properties?.['host']?.length) {
|
||||
validation.error("Field SSH 'Host' can't be empty");
|
||||
}
|
||||
|
||||
const port = Number(this.state.properties?.['port']);
|
||||
if (Number.isNaN(port) || port < 1) {
|
||||
validation.error("Field SSH 'Port' can't be empty");
|
||||
}
|
||||
|
||||
const keyAuth = this.state.authType === NetworkHandlerAuthType.PublicKey;
|
||||
const keySaved = this.initialState?.key === '';
|
||||
if (keyAuth && this.state.savePassword && !keySaved && !this.state.key?.length) {
|
||||
validation.error("Field SSH 'Private key' can't be empty");
|
||||
}
|
||||
|
||||
const passwordSaved = this.initialState?.password === '' && this.initialState?.authType === this.state.authType;
|
||||
|
||||
if (!keyAuth && this.state.savePassword && !passwordSaved && !this.state.password?.length) {
|
||||
validation.error("Field SSH 'Password' can't be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTrimmedSSHConfig(input: NetworkHandlerConfigInput): NetworkHandlerConfigInput {
|
||||
const trimmedInput = toJS(input);
|
||||
const attributesToTrim = Object.keys(input) as (keyof NetworkHandlerConfigInput)[];
|
||||
|
||||
for (const key of attributesToTrim) {
|
||||
if (typeof trimmedInput[key] === 'string') {
|
||||
trimmedInput[key] = trimmedInput[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in trimmedInput.properties) {
|
||||
if (typeof trimmedInput.properties[key] === 'string') {
|
||||
trimmedInput.properties[key] = trimmedInput.properties[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return trimmedInput;
|
||||
}
|
||||
|
||||
function isPasswordChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) {
|
||||
if (!initial && !handler.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(((initial?.password === null && handler.password !== null) || initial?.password === '') && handler.password !== '') || !!handler.password?.length
|
||||
);
|
||||
}
|
||||
|
||||
function isKeyChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) {
|
||||
if (!initial && !handler.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (((initial?.key === null && handler.key !== null) || initial?.key === '') && handler.key !== '') || !!handler.key?.length;
|
||||
}
|
||||
+17
-228
@@ -1,30 +1,25 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, makeObservable, toJS } from 'mobx';
|
||||
import React from 'react';
|
||||
|
||||
import { DBDriverResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { DriverConfigurationType, NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk';
|
||||
import { formStateContext } from '@cloudbeaver/core-ui';
|
||||
import { DriverConfigurationType } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js';
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import { connectionConfigContext } from '../Contexts/connectionConfigContext.js';
|
||||
import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext.js';
|
||||
import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
import { getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource';
|
||||
|
||||
export const SSHTab = React.lazy(async () => {
|
||||
const SSHTab = React.lazy(async () => {
|
||||
const { SSHTab } = await import('./SSHTab.js');
|
||||
return { default: SSHTab };
|
||||
});
|
||||
export const SSHPanel = React.lazy(async () => {
|
||||
const SSHPanel = React.lazy(async () => {
|
||||
const { SSHPanel } = await import('./SSHPanel.js');
|
||||
return { default: SSHPanel };
|
||||
});
|
||||
@@ -32,28 +27,29 @@ export const SSHPanel = React.lazy(async () => {
|
||||
@injectable()
|
||||
export class ConnectionSSHTabService extends Bootstrap {
|
||||
constructor(
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly dbDriverResource: DBDriverResource,
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
) {
|
||||
super();
|
||||
|
||||
makeObservable<this, 'fillConfig' | 'prepareConfig'>(this, {
|
||||
fillConfig: action,
|
||||
prepareConfig: action,
|
||||
});
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.tabsContainer.add({
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'ssh',
|
||||
name: 'plugin_connections_connection_form_part_main',
|
||||
order: 3,
|
||||
tab: () => SSHTab,
|
||||
panel: () => SSHPanel,
|
||||
getLoader: (_, props) => {
|
||||
const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null;
|
||||
return [getCachedMapResourceLoaderState(this.dbDriverResource, () => optionsPart?.state.driverId ?? null)];
|
||||
},
|
||||
isHidden: (tabId, props) => {
|
||||
if (props?.state.config.driverId) {
|
||||
const driver = this.dbDriverResource.get(props.state.config.driverId);
|
||||
const urlType = props.state.config.configurationType === DriverConfigurationType.Url;
|
||||
const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null;
|
||||
|
||||
if (optionsPart?.state.driverId) {
|
||||
const driver = this.dbDriverResource.get(optionsPart.state.driverId);
|
||||
const urlType = optionsPart.state.configurationType === DriverConfigurationType.Url;
|
||||
|
||||
return urlType || !driver?.applicableNetworkHandlers.includes(SSH_TUNNEL_ID);
|
||||
}
|
||||
@@ -61,212 +57,5 @@ export class ConnectionSSHTabService extends Bootstrap {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this));
|
||||
|
||||
this.connectionFormService.formValidationTask.addHandler(this.validate.bind(this));
|
||||
|
||||
this.connectionFormService.formStateTask.addHandler(this.formState.bind(this));
|
||||
|
||||
this.connectionFormService.configureTask.addHandler(this.configure.bind(this));
|
||||
|
||||
this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this));
|
||||
}
|
||||
|
||||
private fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider<IConnectionFormFillConfigData>) {
|
||||
if (!updated) {
|
||||
return;
|
||||
}
|
||||
const initialConfig = state.info?.networkHandlersConfig?.find(handler => handler.id === SSH_TUNNEL_ID);
|
||||
|
||||
if (!state.config.networkHandlersConfig) {
|
||||
state.config.networkHandlersConfig = [];
|
||||
}
|
||||
|
||||
if (!state.config.networkHandlersConfig.some(state => state.id === SSH_TUNNEL_ID)) {
|
||||
state.config.networkHandlersConfig.push({
|
||||
id: SSH_TUNNEL_ID,
|
||||
enabled: false,
|
||||
authType: NetworkHandlerAuthType.Password,
|
||||
password: '',
|
||||
savePassword: false,
|
||||
userName: '',
|
||||
key: '',
|
||||
...initialConfig,
|
||||
properties: {
|
||||
port: 22,
|
||||
host: '',
|
||||
aliveInterval: '0',
|
||||
sshConnectTimeout: '10000',
|
||||
...initialConfig?.properties,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private configure(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const configuration = contexts.getContext(connectionFormConfigureContext);
|
||||
|
||||
configuration.include('includeNetworkHandlersConfig');
|
||||
}
|
||||
|
||||
private validate({ state: { config, info } }: IConnectionFormSubmitData, contexts: IExecutionContextProvider<IConnectionFormSubmitData>) {
|
||||
const validation = contexts.getContext(this.connectionFormService.connectionValidationContext);
|
||||
|
||||
if (!config.networkHandlersConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = config.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID);
|
||||
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handler.enabled) {
|
||||
const initial = info?.networkHandlersConfig?.find(h => h.id === handler.id);
|
||||
if (this.isChanged(handler, initial)) {
|
||||
if (handler.savePassword && !handler.userName?.length) {
|
||||
validation.error("Field SSH 'User' can't be empty");
|
||||
}
|
||||
|
||||
if (!handler.properties?.host?.length) {
|
||||
validation.error("Field SSH 'Host' can't be empty");
|
||||
}
|
||||
|
||||
const port = Number(handler.properties?.port);
|
||||
if (Number.isNaN(port) || port < 1) {
|
||||
validation.error("Field SSH 'Port' can't be empty");
|
||||
}
|
||||
}
|
||||
|
||||
const keyAuth = handler.authType === NetworkHandlerAuthType.PublicKey;
|
||||
const keySaved = initial?.key === '';
|
||||
if (keyAuth && handler.savePassword && !keySaved && !handler.key?.length) {
|
||||
validation.error("Field SSH 'Private key' can't be empty");
|
||||
}
|
||||
|
||||
const passwordSaved = initial?.password === '' && initial.authType === handler.authType;
|
||||
if (!keyAuth && handler.savePassword && !passwordSaved && !handler.password?.length) {
|
||||
validation.error("Field SSH 'Password' can't be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider<IConnectionFormSubmitData>) {
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
const credentialsState = contexts.getContext(connectionCredentialsStateContext);
|
||||
const urlType = state.config.configurationType === DriverConfigurationType.Url;
|
||||
|
||||
if (urlType || !state.config.networkHandlersConfig || state.config.networkHandlersConfig.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let handlerConfig: NetworkHandlerConfigInput | undefined;
|
||||
|
||||
const handler = state.config.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID);
|
||||
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initial = state.info?.networkHandlersConfig?.find(h => h.id === handler.id);
|
||||
const passwordChanged = this.isPasswordChanged(handler, initial);
|
||||
const keyChanged = this.isKeyChanged(handler, initial);
|
||||
|
||||
if (this.isChanged(handler, initial) || passwordChanged || keyChanged) {
|
||||
handlerConfig = {
|
||||
...handler,
|
||||
savePassword: handler.savePassword || config.sharedCredentials,
|
||||
key: handler.authType === NetworkHandlerAuthType.PublicKey && keyChanged ? handler.key : undefined,
|
||||
password: passwordChanged ? handler.password : undefined,
|
||||
};
|
||||
|
||||
delete handlerConfig.secureProperties;
|
||||
}
|
||||
|
||||
if (handler.enabled && !handler.savePassword) {
|
||||
credentialsState.requireNetworkHandler(handler.id);
|
||||
}
|
||||
|
||||
if (handlerConfig) {
|
||||
if (!config.networkHandlersConfig) {
|
||||
config.networkHandlersConfig = [];
|
||||
}
|
||||
|
||||
handlerConfig = this.getTrimmedSSHConfig(handlerConfig);
|
||||
config.networkHandlersConfig.push(handlerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private getTrimmedSSHConfig(input: NetworkHandlerConfigInput): NetworkHandlerConfigInput {
|
||||
const trimmedInput = toJS(input);
|
||||
const attributesToTrim = Object.keys(input) as (keyof NetworkHandlerConfigInput)[];
|
||||
|
||||
for (const key of attributesToTrim) {
|
||||
if (typeof trimmedInput[key] === 'string') {
|
||||
trimmedInput[key] = trimmedInput[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in trimmedInput.properties) {
|
||||
if (typeof trimmedInput.properties[key] === 'string') {
|
||||
trimmedInput.properties[key] = trimmedInput.properties[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return trimmedInput;
|
||||
}
|
||||
|
||||
private formState(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
if (config.networkHandlersConfig !== undefined) {
|
||||
const stateContext = contexts.getContext(formStateContext);
|
||||
|
||||
stateContext.markEdited();
|
||||
}
|
||||
}
|
||||
|
||||
private isChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) {
|
||||
if (!initial && !handler.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const port = Number(initial?.properties?.port);
|
||||
const formPort = Number(handler.properties?.port);
|
||||
|
||||
if (
|
||||
handler.enabled !== initial?.enabled ||
|
||||
handler.authType !== initial?.authType ||
|
||||
handler.savePassword !== initial?.savePassword ||
|
||||
handler.userName !== initial?.userName ||
|
||||
handler.properties?.host !== initial?.properties?.host ||
|
||||
port !== formPort ||
|
||||
handler.properties?.aliveInterval !== initial?.properties?.aliveInterval ||
|
||||
handler.properties?.sshConnectTimeout !== initial?.properties?.sshConnectTimeout
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private isPasswordChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) {
|
||||
if (!initial && !handler.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(((initial?.password === null && handler.password !== null) || initial?.password === '') && handler.password !== '') ||
|
||||
!!handler.password?.length
|
||||
);
|
||||
}
|
||||
|
||||
private isKeyChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) {
|
||||
if (!initial && !handler.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (((initial?.key === null && handler.key !== null) || initial?.key === '') && handler.key !== '') || !!handler.key?.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { schema } from '@cloudbeaver/core-utils';
|
||||
import { CONNECTION_NETWORK_HANDLER_SCHEMA } from '../Options/IConnectionNetworkHanler.js';
|
||||
|
||||
export const CONNECTION_FORM_SSH_SCHEMA = schema.object({}).extend(CONNECTION_NETWORK_HANDLER_SCHEMA.shape);
|
||||
|
||||
export type IConnectionFromSSHState = schema.infer<typeof CONNECTION_FORM_SSH_SCHEMA>;
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
s,
|
||||
Switch,
|
||||
useAdministrationSettings,
|
||||
useAutoLoad,
|
||||
useResource,
|
||||
useS,
|
||||
useTranslate,
|
||||
@@ -31,27 +32,31 @@ import { useService } from '@cloudbeaver/core-di';
|
||||
import { ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk';
|
||||
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
import { useTab, type IFormState, type TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
import { isSafari } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IConnectionFormProps } from '../IConnectionFormProps.js';
|
||||
import { authTypes } from './authTypes.js';
|
||||
import styles from './SSH.module.css';
|
||||
import { SSHKeyUploader } from './SSHKeyUploader.js';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import { getConnectionFormSSHPart } from './getConnectionFormSSHPart.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
interface Props extends IConnectionFormProps {
|
||||
interface Props {
|
||||
handlerState: NetworkHandlerConfigInput;
|
||||
formState: IFormState<IConnectionFormState>;
|
||||
}
|
||||
|
||||
export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ state: formState, handlerState }) {
|
||||
const { info, readonly, disabled: formDisabled } = formState;
|
||||
export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ formState, handlerState, tabId }) {
|
||||
const { selected } = useTab(tabId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { credentialsSavingEnabled } = useAdministrationSettings();
|
||||
const serverConfigResource = useResource(SSH, ServerConfigResource, undefined);
|
||||
|
||||
const initialConfig = info?.networkHandlersConfig?.find(handler => handler.id === SSH_TUNNEL_ID);
|
||||
const serverConfigResource = useResource(SSH, ServerConfigResource, undefined, {
|
||||
active: selected,
|
||||
});
|
||||
|
||||
const resource = useResource(SSH, NetworkHandlerResource, SSH_TUNNEL_ID, {
|
||||
active: selected,
|
||||
onData: handler => {
|
||||
if (Object.keys(handlerState).length === 0) {
|
||||
for (const property of handler.properties) {
|
||||
@@ -69,18 +74,20 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const SSHPart = getConnectionFormSSHPart(formState);
|
||||
const style = useS(styles);
|
||||
const translate = useTranslate();
|
||||
const disabled = formDisabled || loading;
|
||||
const disabled = formState.isDisabled || loading || formState.isReadOnly;
|
||||
const enabled = handlerState.enabled || false;
|
||||
const keyAuth = handlerState.authType === NetworkHandlerAuthType.PublicKey;
|
||||
const passwordFilled = (initialConfig?.password === null && handlerState.password !== '') || !!handlerState.password?.length;
|
||||
const passwordFilled = (SSHPart.initialState?.password === null && handlerState.password !== '') || !!handlerState.password?.length;
|
||||
const testAvailable = keyAuth ? !!handlerState.key?.length : passwordFilled;
|
||||
const passwordLabel = keyAuth ? 'Passphrase' : translate('connections_network_handler_ssh_tunnel_password');
|
||||
const passwordSaved = initialConfig?.password === '' && initialConfig.authType === handlerState.authType;
|
||||
const keySaved = initialConfig?.key === '';
|
||||
const passwordSaved = SSHPart.initialState?.password === '' && SSHPart.initialState.authType === handlerState.authType;
|
||||
const keySaved = SSHPart.initialState?.key === '';
|
||||
const projectInfoResource = useService(ProjectInfoResource);
|
||||
const isSharedProject = projectInfoResource.isProjectShared(formState.projectId);
|
||||
const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
|
||||
const aliveIntervalLabel = translate('connections_network_handler_ssh_tunnel_advanced_settings_alive_interval');
|
||||
const connectTimeoutLabel = translate('connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout');
|
||||
@@ -89,11 +96,13 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
handlerState.password = '';
|
||||
}, []);
|
||||
|
||||
useAutoLoad(SSH, [SSHPart, optionsPart], selected);
|
||||
|
||||
return (
|
||||
<Form className={s(style, { form: true })}>
|
||||
<ColoredContainer parent>
|
||||
<Group form gap keepSize large>
|
||||
<Switch id="ssh-enable-switch" name="enabled" state={handlerState} mod={['primary']} disabled={disabled || readonly}>
|
||||
<Switch id="ssh-enable-switch" name="enabled" state={handlerState} mod={['primary']} disabled={disabled}>
|
||||
{translate('connections_network_handler_ssh_tunnel_enable')}
|
||||
</Switch>
|
||||
<Combobox
|
||||
@@ -102,17 +111,17 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
items={authTypes}
|
||||
keySelector={value => value.key}
|
||||
valueSelector={value => value.label}
|
||||
disabled={disabled || readonly || !enabled}
|
||||
disabled={disabled || !enabled}
|
||||
tiny
|
||||
onSelect={authTypeChangeHandler}
|
||||
>
|
||||
{translate('connections_network_handler_ssh_tunnel_auth_type')}
|
||||
</Combobox>
|
||||
<Container wrap gap>
|
||||
<InputField type="text" name="host" state={handlerState.properties} readOnly={readonly || disabled || !enabled} required small>
|
||||
<InputField type="text" name="host" state={handlerState.properties} readOnly={disabled || !enabled} required small>
|
||||
{translate('connections_network_handler_ssh_tunnel_host')}
|
||||
</InputField>
|
||||
<InputField type="number" name="port" state={handlerState.properties} readOnly={readonly || disabled || !enabled} required tiny>
|
||||
<InputField type="number" name="port" state={handlerState.properties} readOnly={disabled || !enabled} required tiny>
|
||||
{translate('connections_network_handler_ssh_tunnel_port')}
|
||||
</InputField>
|
||||
</Container>
|
||||
@@ -121,7 +130,7 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
type="text"
|
||||
name="userName"
|
||||
state={handlerState}
|
||||
readOnly={readonly || disabled || !enabled}
|
||||
readOnly={disabled || !enabled}
|
||||
required={handlerState.savePassword}
|
||||
tiny
|
||||
fill
|
||||
@@ -133,7 +142,7 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
name="password"
|
||||
autoComplete={isSafari ? 'section-connection-ssh-authentication section-ssh password' : 'new-password'}
|
||||
state={handlerState}
|
||||
readOnly={readonly || disabled || !enabled}
|
||||
readOnly={disabled || !enabled}
|
||||
required={!passwordSaved && !keyAuth && handlerState.savePassword}
|
||||
description={passwordSaved ? translate('ui_processing_saved') : undefined}
|
||||
tiny
|
||||
@@ -141,9 +150,9 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
>
|
||||
{passwordLabel}
|
||||
</InputField>
|
||||
{keyAuth && <SSHKeyUploader state={handlerState} saved={keySaved} disabled={disabled || !enabled} readonly={readonly} />}
|
||||
{keyAuth && <SSHKeyUploader state={handlerState} saved={keySaved} disabled={disabled || !enabled} readonly={formState.isReadOnly} />}
|
||||
</Container>
|
||||
{credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && (
|
||||
{credentialsSavingEnabled && !optionsPart.state.template && !optionsPart.state.sharedCredentials && (
|
||||
<FieldCheckbox
|
||||
id={SSH_TUNNEL_ID + '_savePassword'}
|
||||
title={translate(
|
||||
@@ -153,7 +162,7 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
)}
|
||||
name="savePassword"
|
||||
state={handlerState}
|
||||
disabled={disabled || !enabled || readonly}
|
||||
disabled={disabled || !enabled}
|
||||
>
|
||||
{translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
@@ -169,7 +178,7 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
type="number"
|
||||
name="aliveInterval"
|
||||
state={handlerState.properties}
|
||||
readOnly={readonly || disabled || !enabled}
|
||||
readOnly={disabled || !enabled}
|
||||
labelTooltip={aliveIntervalLabel}
|
||||
tiny
|
||||
>
|
||||
@@ -179,7 +188,7 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
type="number"
|
||||
name="sshConnectTimeout"
|
||||
state={handlerState.properties}
|
||||
readOnly={readonly || disabled || !enabled}
|
||||
readOnly={disabled || !enabled}
|
||||
labelTooltip={connectTimeoutLabel}
|
||||
tiny
|
||||
>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 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 { SSH_TUNNEL_ID } from '@cloudbeaver/core-connections';
|
||||
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
|
||||
import type { IConnectionFormProps } from '../IConnectionFormProps.js';
|
||||
import { SSH } from './SSH.js';
|
||||
import { useAutoLoad } from '@cloudbeaver/core-blocks';
|
||||
import { getConnectionFormSSHPart } from './getConnectionFormSSHPart.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
|
||||
export const SSHPanel: TabContainerPanelComponent<IConnectionFormProps> = observer(function SSHPanel(props) {
|
||||
const state = props.state.config.networkHandlersConfig?.find(state => state.id === SSH_TUNNEL_ID);
|
||||
const sshPart = getConnectionFormSSHPart(props.formState);
|
||||
|
||||
if (!state) {
|
||||
useAutoLoad(SSHPanel, sshPart);
|
||||
|
||||
if (!sshPart.state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SSH {...props} handlerState={state} />;
|
||||
return <SSH {...props} handlerState={sshPart.state} />;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -9,12 +9,14 @@ import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Translate, useResource } from '@cloudbeaver/core-blocks';
|
||||
import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections';
|
||||
import { Tab, type TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui';
|
||||
|
||||
import type { IConnectionFormProps } from '../IConnectionFormProps.js';
|
||||
import { Tab, type TabContainerTabComponent, TabTitle, useTab } from '@cloudbeaver/core-ui';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
|
||||
export const SSHTab: TabContainerTabComponent<IConnectionFormProps> = observer(function SSHTab(props) {
|
||||
const handler = useResource(SSHTab, NetworkHandlerResource, SSH_TUNNEL_ID);
|
||||
const { selected } = useTab(props.tabId);
|
||||
const handler = useResource(SSHTab, NetworkHandlerResource, SSH_TUNNEL_ID, {
|
||||
active: selected,
|
||||
});
|
||||
|
||||
return (
|
||||
<Tab {...props} title={handler.data?.description}>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context';
|
||||
import type { IFormState } from '@cloudbeaver/core-ui';
|
||||
import { ConnectionFormSSHPart } from './ConnectionFormSSHPart.js';
|
||||
import { ConnectionInfoNetworkHandlersResource } from '@cloudbeaver/core-connections';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
const DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART = createDataContext<ConnectionFormSSHPart>('Connection Form SSH Part');
|
||||
|
||||
export function getConnectionFormSSHPart(formState: IFormState<IConnectionFormState>): ConnectionFormSSHPart {
|
||||
return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART, context => {
|
||||
const di = context.get(DATA_CONTEXT_DI_PROVIDER)!;
|
||||
const connectionInfoNetworkHandlersResource = di.getService(ConnectionInfoNetworkHandlersResource);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
|
||||
return new ConnectionFormSSHPart(formState, connectionInfoNetworkHandlersResource, optionsPart);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { FormPart, formSubmitContext, type IFormState } from '@cloudbeaver/core-ui';
|
||||
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import { type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk';
|
||||
import { isNotNullDefined } from '@cloudbeaver/core-utils';
|
||||
import { getSSLDriverHandler } from './getSSLDriverHandler.js';
|
||||
import { ConnectionInfoNetworkHandlersResource, type DBDriverResource, type NetworkHandlerResource } from '@cloudbeaver/core-connections';
|
||||
import { CachedMapAllKey } from '@cloudbeaver/core-resource';
|
||||
import { makeObservable, observable, toJS } from 'mobx';
|
||||
import { PROPERTY_FEATURE_SECURED } from './PROPERTY_FEATURE_SECURED.js';
|
||||
import { SSL_CODE_NAME } from './SSL_CODE_NAME.js';
|
||||
import type { INetworkHandlerConfig } from '../Options/IConnectionNetworkHanler.js';
|
||||
import { getSSLDefaultConfig } from './getSSLDefaultConfig.js';
|
||||
import { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js';
|
||||
|
||||
const getDefaultState = () =>
|
||||
({
|
||||
id: SSL_CODE_NAME,
|
||||
enabled: false,
|
||||
properties: {},
|
||||
secureProperties: {},
|
||||
}) as INetworkHandlerConfig;
|
||||
|
||||
export class ConnectionFormSSLPart extends FormPart<INetworkHandlerConfig, IConnectionFormState> {
|
||||
private activeDriverId: string | undefined;
|
||||
constructor(
|
||||
formState: IFormState<IConnectionFormState>,
|
||||
private readonly dbDriverResource: DBDriverResource,
|
||||
private readonly networkHandlerResource: NetworkHandlerResource,
|
||||
private readonly connectionInfoNetworkHandlersResource: ConnectionInfoNetworkHandlersResource,
|
||||
private readonly optionsPart: ConnectionFormOptionsPart,
|
||||
) {
|
||||
super(formState, getDefaultState());
|
||||
|
||||
this.activeDriverId = undefined;
|
||||
|
||||
makeObservable<this, 'activeDriverId'>(this, {
|
||||
activeDriverId: observable,
|
||||
});
|
||||
}
|
||||
|
||||
override isLoaded(): boolean {
|
||||
return super.isLoaded() && this.activeDriverId === this.optionsPart.state.driverId;
|
||||
}
|
||||
|
||||
override isOutdated(): boolean {
|
||||
const isDriverOutdated = Boolean(this.optionsPart.state.driverId && this.dbDriverResource.isOutdated(this.optionsPart.state.driverId));
|
||||
const isNetworkHandlerOutdated = this.networkHandlerResource.isOutdated(CachedMapAllKey);
|
||||
const isConnectionInfoNetworkHandlersOutdated = this.optionsPart.connectionKey
|
||||
? this.connectionInfoNetworkHandlersResource.isOutdated(this.optionsPart.connectionKey)
|
||||
: false;
|
||||
|
||||
return isDriverOutdated || isNetworkHandlerOutdated || isConnectionInfoNetworkHandlersOutdated;
|
||||
}
|
||||
|
||||
protected override async loader(): Promise<void> {
|
||||
this.activeDriverId = this.optionsPart.state.driverId;
|
||||
|
||||
if (!this.optionsPart.state.driverId) {
|
||||
this.setInitialState(getDefaultState());
|
||||
return;
|
||||
}
|
||||
|
||||
const [driver, handlers] = await Promise.all([
|
||||
this.dbDriverResource.load(this.optionsPart.state.driverId),
|
||||
this.networkHandlerResource.load(CachedMapAllKey),
|
||||
]);
|
||||
const handler = getSSLDriverHandler(handlers, driver?.applicableNetworkHandlers ?? []);
|
||||
|
||||
if (!handler) {
|
||||
this.setInitialState(getDefaultState());
|
||||
return;
|
||||
}
|
||||
|
||||
const info = this.optionsPart.connectionKey ? await this.connectionInfoNetworkHandlersResource.load(this.optionsPart.connectionKey) : null;
|
||||
const initialConfig = info?.networkHandlersConfig?.find(h => h.id === handler.id);
|
||||
|
||||
if (!this.optionsPart.state.networkHandlersConfig?.some(state => state.id === handler.id)) {
|
||||
const config: NetworkHandlerConfigInput = initialConfig ? toJS(initialConfig) : getSSLDefaultConfig(handler.id);
|
||||
|
||||
if (config.secureProperties) {
|
||||
config.properties = { ...config.properties, ...config.secureProperties };
|
||||
}
|
||||
|
||||
this.setInitialState(config);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setInitialState(initialConfig ?? getDefaultState());
|
||||
}
|
||||
|
||||
protected override async format(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {
|
||||
if (!this.optionsPart.state.driverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlers = await this.networkHandlerResource.load(CachedMapAllKey);
|
||||
const descriptor = handlers.find(h => h.id === this.state?.id);
|
||||
|
||||
const handlerConfig: NetworkHandlerConfigInput = toJS(this.state);
|
||||
handlerConfig.savePassword = this.state.savePassword || this.optionsPart.state.sharedCredentials;
|
||||
|
||||
if (this.isChanged && descriptor) {
|
||||
for (const descriptorProperty of descriptor.properties) {
|
||||
if (!descriptorProperty.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = descriptorProperty.id;
|
||||
const isDefault = isNotNullDefined(descriptorProperty.defaultValue);
|
||||
|
||||
if (!(key in handlerConfig.properties) && isDefault) {
|
||||
handlerConfig.properties[key] = descriptorProperty.defaultValue;
|
||||
}
|
||||
|
||||
const secured = descriptorProperty.features.includes(PROPERTY_FEATURE_SECURED);
|
||||
|
||||
if (secured) {
|
||||
const value = handlerConfig.properties[key];
|
||||
const propertyChanged = this.initialState?.secureProperties?.[key] !== value;
|
||||
|
||||
if (propertyChanged) {
|
||||
handlerConfig.secureProperties[key] = toJS(value);
|
||||
} else {
|
||||
delete handlerConfig.secureProperties[key];
|
||||
}
|
||||
|
||||
delete handlerConfig.properties[key];
|
||||
}
|
||||
}
|
||||
|
||||
const submitInfo = contexts.getContext(formSubmitContext);
|
||||
if (submitInfo.type === 'submit') {
|
||||
if (Object.keys(handlerConfig.secureProperties).length === 0) {
|
||||
delete handlerConfig.secureProperties;
|
||||
}
|
||||
|
||||
if (Object.keys(handlerConfig.properties).length === 0) {
|
||||
delete handlerConfig.properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.enabled && !this.state.savePassword) {
|
||||
this.formState.state.requiredNetworkHandlersIds.push(this.state.id);
|
||||
} else if (!this.state.enabled) {
|
||||
this.formState.state.requiredNetworkHandlersIds = this.formState.state.requiredNetworkHandlersIds.filter(id => id !== this.state.id);
|
||||
}
|
||||
|
||||
if (handlerConfig) {
|
||||
trimSSLConfig(handlerConfig);
|
||||
|
||||
this.optionsPart.state.networkHandlersConfig!.push(handlerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async saveChanges(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {}
|
||||
}
|
||||
|
||||
function trimSSLConfig(input: INetworkHandlerConfig): INetworkHandlerConfig {
|
||||
const { secureProperties } = input;
|
||||
|
||||
if (!secureProperties) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (!Object.keys(secureProperties).length) {
|
||||
return input;
|
||||
}
|
||||
|
||||
for (const key in secureProperties) {
|
||||
if (typeof secureProperties[key] === 'string') {
|
||||
secureProperties[key] = secureProperties[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
+16
-196
@@ -1,36 +1,24 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, makeObservable, toJS } from 'mobx';
|
||||
import React from 'react';
|
||||
|
||||
import { DBDriverResource, NetworkHandlerResource } from '@cloudbeaver/core-connections';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { CachedMapAllKey } from '@cloudbeaver/core-resource';
|
||||
import type { NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk';
|
||||
import { formStateContext } from '@cloudbeaver/core-ui';
|
||||
import { isNotNullDefined, isObjectsEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js';
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import { connectionConfigContext } from '../Contexts/connectionConfigContext.js';
|
||||
import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext.js';
|
||||
import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps.js';
|
||||
import { getSSLDefaultConfig } from './getSSLDefaultConfig.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
import { getSSLDriverHandler } from './getSSLDriverHandler.js';
|
||||
import { PROPERTY_FEATURE_SECURED } from './PROPERTY_FEATURE_SECURED.js';
|
||||
import { SSL_CODE_NAME } from './SSL_CODE_NAME.js';
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource';
|
||||
|
||||
export const SSLTab = React.lazy(async () => {
|
||||
const SSLTab = React.lazy(async () => {
|
||||
const { SSLTab } = await import('./SSLTab.js');
|
||||
return { default: SSLTab };
|
||||
});
|
||||
export const SSLPanel = React.lazy(async () => {
|
||||
const SSLPanel = React.lazy(async () => {
|
||||
const { SSLPanel } = await import('./SSLPanel.js');
|
||||
return { default: SSLPanel };
|
||||
});
|
||||
@@ -38,27 +26,28 @@ export const SSLPanel = React.lazy(async () => {
|
||||
@injectable()
|
||||
export class ConnectionSSLTabService extends Bootstrap {
|
||||
constructor(
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly dbDriverResource: DBDriverResource,
|
||||
private readonly networkHandlerResource: NetworkHandlerResource,
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
) {
|
||||
super();
|
||||
|
||||
makeObservable<this, 'fillConfig' | 'prepareConfig'>(this, {
|
||||
fillConfig: action,
|
||||
prepareConfig: action,
|
||||
});
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.tabsContainer.add({
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'ssl',
|
||||
order: 4,
|
||||
tab: () => SSLTab,
|
||||
panel: () => SSLPanel,
|
||||
getLoader: () => [
|
||||
getCachedMapResourceLoaderState(this.dbDriverResource, () => CachedMapAllKey),
|
||||
getCachedMapResourceLoaderState(this.networkHandlerResource, () => CachedMapAllKey),
|
||||
],
|
||||
isHidden: (_, props) => {
|
||||
if (props?.state.config.driverId) {
|
||||
const driver = this.dbDriverResource.get(props.state.config.driverId);
|
||||
const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null;
|
||||
|
||||
if (optionsPart?.state.driverId) {
|
||||
const driver = this.dbDriverResource.get(optionsPart.state.driverId);
|
||||
const handler = getSSLDriverHandler(this.networkHandlerResource.values, driver?.applicableNetworkHandlers ?? []);
|
||||
return !handler;
|
||||
}
|
||||
@@ -66,174 +55,5 @@ export class ConnectionSSLTabService extends Bootstrap {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this));
|
||||
|
||||
this.connectionFormService.formStateTask.addHandler(this.formState.bind(this));
|
||||
|
||||
this.connectionFormService.configureTask.addHandler(this.configure.bind(this));
|
||||
|
||||
this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this));
|
||||
}
|
||||
|
||||
private async fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider<IConnectionFormFillConfigData>) {
|
||||
if (!updated || !state.config.driverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const driver = await this.dbDriverResource.load(state.config.driverId);
|
||||
const handlers = await this.networkHandlerResource.load(CachedMapAllKey);
|
||||
|
||||
const handler = getSSLDriverHandler(handlers, driver?.applicableNetworkHandlers ?? []);
|
||||
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialConfig = state.info?.networkHandlersConfig?.find(h => h.id === handler.id);
|
||||
|
||||
if (!state.config.networkHandlersConfig) {
|
||||
state.config.networkHandlersConfig = [];
|
||||
}
|
||||
|
||||
if (!state.config.networkHandlersConfig.some(state => state.id === handler.id)) {
|
||||
const config: NetworkHandlerConfigInput = initialConfig ? toJS(initialConfig) : getSSLDefaultConfig(handler.id);
|
||||
|
||||
if (config.secureProperties) {
|
||||
config.properties = { ...config.properties, ...config.secureProperties };
|
||||
}
|
||||
|
||||
state.config.networkHandlersConfig.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
private configure(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const configuration = contexts.getContext(connectionFormConfigureContext);
|
||||
|
||||
configuration.include('includeNetworkHandlersConfig');
|
||||
}
|
||||
|
||||
private async prepareConfig({ state, submitType }: IConnectionFormSubmitData, contexts: IExecutionContextProvider<IConnectionFormSubmitData>) {
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
const credentialsState = contexts.getContext(connectionCredentialsStateContext);
|
||||
|
||||
if (!state.config.networkHandlersConfig || state.config.networkHandlersConfig.length === 0 || !state.config.driverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const driver = await this.dbDriverResource.load(state.config.driverId);
|
||||
const handlers = await this.networkHandlerResource.load(CachedMapAllKey);
|
||||
const handler = state.config.networkHandlersConfig.find(
|
||||
handler => driver?.applicableNetworkHandlers.includes(handler.id) && handlers.some(h => h.id === handler.id && h.codeName === SSL_CODE_NAME),
|
||||
);
|
||||
const descriptor = handlers.find(h => h.id === handler?.id);
|
||||
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initial = state.info?.networkHandlersConfig?.find(h => h.id === handler.id);
|
||||
const handlerConfig: NetworkHandlerConfigInput = toJS(handler);
|
||||
handlerConfig.savePassword = handler.savePassword || config.sharedCredentials;
|
||||
|
||||
const changed = this.isChanged(handlerConfig, initial);
|
||||
|
||||
if (changed && descriptor) {
|
||||
for (const descriptorProperty of descriptor.properties) {
|
||||
if (!descriptorProperty.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = descriptorProperty.id;
|
||||
const isDefault = isNotNullDefined(descriptorProperty.defaultValue);
|
||||
|
||||
if (!(key in handlerConfig.properties) && isDefault) {
|
||||
handlerConfig.properties[key] = descriptorProperty.defaultValue;
|
||||
}
|
||||
|
||||
const secured = descriptorProperty.features.includes(PROPERTY_FEATURE_SECURED);
|
||||
|
||||
if (secured) {
|
||||
const value = handlerConfig.properties[key];
|
||||
const propertyChanged = initial?.secureProperties?.[key] !== value;
|
||||
|
||||
if (propertyChanged) {
|
||||
handlerConfig.secureProperties[key] = toJS(value);
|
||||
} else {
|
||||
delete handlerConfig.secureProperties[key];
|
||||
}
|
||||
|
||||
delete handlerConfig.properties[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (submitType === 'submit') {
|
||||
if (Object.keys(handlerConfig.secureProperties).length === 0) {
|
||||
delete handlerConfig.secureProperties;
|
||||
}
|
||||
|
||||
if (Object.keys(handlerConfig.properties).length === 0) {
|
||||
delete handlerConfig.properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (handler.enabled && !handler.savePassword) {
|
||||
credentialsState.requireNetworkHandler(handler.id);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
if (!config.networkHandlersConfig) {
|
||||
config.networkHandlersConfig = [];
|
||||
}
|
||||
|
||||
this.trimSSLConfig(handlerConfig);
|
||||
config.networkHandlersConfig.push(handlerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private trimSSLConfig(input: NetworkHandlerConfigInput) {
|
||||
const { secureProperties } = input;
|
||||
|
||||
if (!secureProperties) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Object.keys(secureProperties).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in secureProperties) {
|
||||
if (typeof secureProperties[key] === 'string') {
|
||||
secureProperties[key] = secureProperties[key]?.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formState(data: IConnectionFormState, contexts: IExecutionContextProvider<IConnectionFormState>) {
|
||||
const config = contexts.getContext(connectionConfigContext);
|
||||
if (config.networkHandlersConfig !== undefined) {
|
||||
const stateContext = contexts.getContext(formStateContext);
|
||||
|
||||
stateContext.markEdited();
|
||||
}
|
||||
}
|
||||
|
||||
private isChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) {
|
||||
if (!initial && !handler.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const initialProperties = { ...(initial?.properties ?? {}), ...(initial?.secureProperties ?? {}) };
|
||||
|
||||
if (
|
||||
handler.enabled !== initial?.enabled ||
|
||||
handler.savePassword !== initial?.savePassword ||
|
||||
!isObjectsEqual(handler.properties, initialProperties)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { schema } from '@cloudbeaver/core-utils';
|
||||
|
||||
export const CONNECTION_FORM_SSL_SCHEMA = schema.object({});
|
||||
|
||||
export type IConnectionFromSSLState = schema.infer<typeof CONNECTION_FORM_SSL_SCHEMA>;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user