Merge branch 'devel' into dbeaver/pro#9733-Remove-X-prefix-HTTP-headers

This commit is contained in:
Daria Fedorova
2026-08-09 23:09:45 +04:00
committed by GitHub
93 changed files with 897 additions and 335 deletions
@@ -1027,6 +1027,9 @@ public class LocalResourceController extends BaseLocalResourceController {
}
try {
while (resourcePath.startsWith("/")) resourcePath = resourcePath.substring(1);
for (Path part : projectPath.resolve(resourcePath)) {
GeneralUtils.validateResourceNameUnconditionally(part.toString());
}
Path targetPath = projectPath.resolve(resourcePath).normalize();
if (!targetPath.startsWith(projectPath)) {
throw new DBException("Invalid resource path");
@@ -59,6 +59,7 @@ public class WebAuthProviderDescriptor extends AbstractDescriptor {
private final boolean isPrivate;
private final boolean isAuthHidden;
private final boolean isCaseInsensitive;
private final boolean sameTabRedirectOnLogout;
private final boolean serviceProvider;
private final String[] requiredFeatures;
private final boolean isRequired;
@@ -75,6 +76,7 @@ public class WebAuthProviderDescriptor extends AbstractDescriptor {
this.isRequired = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_REQUIRED));
this.isAuthHidden = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_AUTH_HIDDEN));
this.isCaseInsensitive = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_CASE_INSENSITIVE));
this.sameTabRedirectOnLogout = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_SAME_TAB_LOGOUT_REDIRECT));
this.serviceProvider = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_SERVICE_PROVIDER));
for (IConfigurationElement cfgElement : cfg.getChildren(WebRegistryConstant.TAG_CONFIGURATION)) {
@@ -129,6 +131,10 @@ public class WebAuthProviderDescriptor extends AbstractDescriptor {
return trusted;
}
public boolean isSameTabRedirectOnLogout() {
return sameTabRedirectOnLogout;
}
public boolean isPrivate() {
return isPrivate;
}
@@ -29,6 +29,7 @@ public class WebRegistryConstant {
public static final String ATTR_REQUIRED = "required";
public static final String ATTR_AUTH_HIDDEN = "authHidden";
public static final String ATTR_CASE_INSENSITIVE = "caseInsensitive";
public static final String ATTR_SAME_TAB_LOGOUT_REDIRECT = "sameTabRedirectOnLogout";
public static final String ATTR_REQUIRED_FEATURES = "requiredFeatures";
public static final String ATTR_CATEGORIES = "categories";
public static final String ATTR_SERVICE_PROVIDER = "serviceProvider";
@@ -17,7 +17,7 @@
</extension>
<extension point="io.cloudbeaver.feature">
<feature id="ai" label="AI Integration" description="Enables AI Chat in the application."/>
<feature id="ai" label="AI Integration" description="Enables AI Chat in the application." enabledByDefault="true"/>
</extension>
<extension point="org.jkiss.dbeaver.application">
@@ -108,8 +108,14 @@ type FederatedAuthResult @since(version: "25.0.3") {
userTokens: [UserAuthToken!]! @since(version: "25.0.3")
}
type LogoutLink @since(version: "26.1.4") {
url: String!
"If true, the URL must be opened as a full-page redirect in the current tab instead of a popup window"
sameTabRedirect: Boolean!
}
type LogoutInfo @since(version: "23.3.3") {
redirectLinks: [String!]!
redirectLinks: [LogoutLink!]!
}
type UserAuthToken {
@@ -20,5 +20,5 @@ import org.jkiss.code.NotNull;
import java.util.List;
public record WebLogoutInfo(@NotNull List<String> redirectLinks) {
public record WebLogoutInfo(@NotNull List<WebLogoutLink> redirectLinks) {
}
@@ -0,0 +1,29 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.auth;
import org.jkiss.code.NotNull;
/**
* A single logout redirect link.
*
* @param url the URL to open on logout
* @param sameTabRedirect if true, the URL must be opened as a full-page redirect
* in the current tab instead of a popup window
*/
public record WebLogoutLink(@NotNull String url, boolean sameTabRedirect) {
}
@@ -249,7 +249,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
List<WebAuthInfo> removedInfos = webSession.removeAuthInfo(providerId);
var cbApp = CBApplication.getInstance();
List<String> logoutUrls = new ArrayList<>();
List<WebLogoutLink> redirectLinks = new ArrayList<>();
String origin = ServletAppUtils.getOriginFromRequest(httpRequest);
for (WebAuthInfo removedInfo : removedInfos) {
if (removedInfo.getAuthProviderDescriptor()
@@ -274,11 +274,14 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
);
}
if (CommonUtils.isNotEmpty(logoutUrl)) {
logoutUrls.add(logoutUrl);
redirectLinks.add(new WebLogoutLink(
logoutUrl,
removedInfo.getAuthProviderDescriptor().isSameTabRedirectOnLogout()
));
}
}
}
return new WebLogoutInfo(logoutUrls);
return new WebLogoutInfo(redirectLinks);
} catch (DBException e) {
throw new DBWebException("User logout failed", e);
}
@@ -20,6 +20,7 @@
description="Reverse proxy header based authentication"
trusted="true"
configurable="true"
sameTabRedirectOnLogout="true"
class="io.cloudbeaver.auth.provider.rp.RPAuthProvider"
icon="platform:/plugin/org.jkiss.dbeaver.model/icons/tree/key.svg">
<credentials>
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,7 +48,10 @@ public class AuthenticationTest extends CloudbeaverMockTest {
private static final String GQL_AUTH_LOGOUT = """
query authLogoutExtended($provider: ID, $configuration: ID) {
result: authLogoutExtended(provider: $provider, configuration: $configuration) {
redirectLinks
redirectLinks {
url
sameTabRedirect
}
}
}""";
@@ -34,6 +34,7 @@
"dependencies": {
"async-mutex": "^0",
"color": "^5.0.2",
"modern-screenshot": "^4",
"p-debounce": "^5",
"p-memoize": "^8.0.0",
"quick-lru": "^7.1.0",
@@ -0,0 +1,37 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 const IMAGE_EXPORT_FORMATS = ['SVG', 'PNG'] as const;
export type ImageExportFormat = (typeof IMAGE_EXPORT_FORMATS)[number];
export interface IImageExportOptions {
fileName: string;
format: ImageExportFormat;
transparent: boolean;
}
/** Turns the requested options into the name the file is saved under, see {@link timestampedImageFileName}. */
export type ImageExportFileNameFormatter = (options: IImageExportOptions) => string;
export interface IImageExportResult {
fileName: string;
format: ImageExportFormat;
blob: Blob;
}
export type ImageExportReadyHandler = (result: IImageExportResult) => void | Promise<void>;
/** For consumers that cannot take a Blob, see {@link toDataUrlImageExport}. */
export interface IImageExportDataUrlResult {
fileName: string;
format: ImageExportFormat;
dataUrl: string;
}
export type ImageExportDataUrlReadyHandler = (result: IImageExportDataUrlResult) => void;
@@ -0,0 +1,16 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 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,13 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { download } from './download.js';
import type { IImageExportResult } from './IImageExportOptions.js';
export function downloadImageExport(result: IImageExportResult): void {
download(result.blob, result.fileName);
}
@@ -0,0 +1,91 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { domToBlob, domToForeignObjectSvg, type Options } from 'modern-screenshot';
import { DEFAULT_MAX_IMAGE_SIDE, fitToPixelBudget, type IPixelBudget } from './fitToPixelBudget.js';
import type { IImageExportOptions, ImageExportFileNameFormatter, ImageExportReadyHandler } from './IImageExportOptions.js';
import { isImageBroken } from './isImageBroken.js';
import { timestampedImageFileName } from './timestampedImageFileName.js';
export const IMAGE_EXPORT_BROKEN_ERROR = 'Something went wrong. Please try to select another file format';
const DEFAULT_BACKGROUND_COLOR = '#ffffff';
const DEFAULT_PNG_SCALE = 2;
export interface IDomImageExportConfig extends IPixelBudget {
/** In CSS pixels, defaults to the bounding box of `node`. Required for nodes with no box of their own. */
width?: number;
height?: number;
/** Used when `options.transparent` is false. */
backgroundColor?: string;
/** Applied to the clone before rendering, the live DOM is left alone. */
style?: Partial<CSSStyleDeclaration>;
/** Return false to keep a node (and its children) out of the snapshot. */
filter?: (node: Node) => boolean;
/** Defaults to {@link timestampedImageFileName}. */
formatFileName?: ImageExportFileNameFormatter;
}
/**
* PNG is scaled down to fit {@link fitToPixelBudget}, SVG is always 1:1
*/
export async function exportDomImage(
node: Element,
options: IImageExportOptions,
onReady: ImageExportReadyHandler,
config?: IDomImageExportConfig,
): Promise<void> {
const { width, height } = resolveSize(node, config);
const scale = options.format === 'PNG' ? fitToPixelBudget(width, height, { ...config, scale: config?.scale ?? DEFAULT_PNG_SCALE }) : 1;
const screenshotOptions: Options = {
// a size given here is forced onto the clone, so pass it on only when asked for
...(config?.width === undefined ? {} : { width: config.width }),
...(config?.height === undefined ? {} : { height: config.height }),
scale,
style: config?.style,
filter: config?.filter,
backgroundColor: options.transparent ? 'transparent' : (config?.backgroundColor ?? DEFAULT_BACKGROUND_COLOR),
quality: 1,
maximumCanvasSize: config?.maxSide ?? DEFAULT_MAX_IMAGE_SIDE,
};
const blob = options.format === 'PNG' ? await renderPng(node, screenshotOptions) : await renderSvg(node, screenshotOptions);
const uri = URL.createObjectURL(blob);
const broken = await isImageBroken(uri);
URL.revokeObjectURL(uri);
if (broken) {
throw new Error(IMAGE_EXPORT_BROKEN_ERROR);
}
const formatFileName = config?.formatFileName ?? timestampedImageFileName;
await onReady({ fileName: formatFileName(options), format: options.format, blob });
}
function renderPng(node: Element, screenshotOptions: Options): Promise<Blob> {
return domToBlob(node, { ...screenshotOptions, type: 'image/png' });
}
async function renderSvg(node: Element, screenshotOptions: Options): Promise<Blob> {
const svg = await domToForeignObjectSvg(node, screenshotOptions);
const svgData = new XMLSerializer().serializeToString(svg);
return new Blob(['<?xml version="1.0" standalone="no"?>\r\n', svgData], { type: 'image/svg+xml;charset=utf-8' });
}
function resolveSize(node: Element, config?: IDomImageExportConfig): { width: number; height: number } {
if (config?.width !== undefined && config.height !== undefined) {
return { width: config.width, height: config.height };
}
const rect = node.getBoundingClientRect();
return { width: rect.width, height: rect.height };
}
@@ -0,0 +1,42 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { describe, expect, test } from 'vitest';
import { DEFAULT_MAX_IMAGE_PIXELS, DEFAULT_MAX_IMAGE_SIDE, fitToPixelBudget } from './fitToPixelBudget.js';
describe('fitToPixelBudget', () => {
test('should keep the requested scale when the content fits', () => {
expect(fitToPixelBudget(1200, 800, { scale: 2 })).toBe(2);
});
test('should default to scale 1', () => {
expect(fitToPixelBudget(1200, 800)).toBe(1);
});
test('should scale down when the total area exceeds the pixel budget', () => {
const scale = fitToPixelBudget(12000, 9000, { scale: 2 });
expect(scale).toBeLessThan(1);
expect(12000 * scale * 9000 * scale).toBeLessThanOrEqual(DEFAULT_MAX_IMAGE_PIXELS + 1);
});
test('should scale down when a single side exceeds the max side', () => {
const scale = fitToPixelBudget(20000, 300, { scale: 1 });
expect(20000 * scale).toBeLessThanOrEqual(DEFAULT_MAX_IMAGE_SIDE + 1);
});
test('should respect custom budgets', () => {
expect(fitToPixelBudget(2000, 2000, { scale: 1, maxSide: 1000 })).toBe(0.5);
expect(fitToPixelBudget(2000, 2000, { scale: 1, maxPixels: 1_000_000 })).toBe(0.5);
});
test('should not collapse to zero on an enormous content', () => {
expect(fitToPixelBudget(10_000_000, 10_000_000, { scale: 1 })).toBeGreaterThan(0);
});
});
@@ -0,0 +1,34 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
/** Canvases stop rendering reliably somewhere around this side length. */
export const DEFAULT_MAX_IMAGE_SIDE = 8192;
/** ~16 Mpx, about 64 MB of RGBA. */
export const DEFAULT_MAX_IMAGE_PIXELS = 16_777_216;
const MIN_SCALE = 0.05;
export interface IPixelBudget {
/** Device pixel ratio to aim for. Defaults to 1. */
scale?: number;
maxSide?: number;
maxPixels?: number;
}
export function fitToPixelBudget(width: number, height: number, budget?: IPixelBudget): number {
const { scale = 1, maxSide = DEFAULT_MAX_IMAGE_SIDE, maxPixels = DEFAULT_MAX_IMAGE_PIXELS } = budget ?? {};
if (width <= 0 || height <= 0) {
return scale;
}
const fitted = Math.min(scale, maxSide / width, maxSide / height, Math.sqrt(maxPixels / (width * height)));
return Math.max(fitted, Math.min(scale, MIN_SCALE));
}
@@ -6,8 +6,18 @@
* you may not use this file except in compliance with the License.
*/
export * from './blobToDataUrl.js';
export * from './ColorConvert.js';
export * from './debouncePromise.js';
export * from './download.js';
export * from './downloadImageExport.js';
export * from './exportDomImage.js';
export * from './fitToPixelBudget.js';
export * from './IImageExportOptions.js';
export * from './isImageBroken.js';
export * from './timestampedImageFileName.js';
export * from './toDataUrlImageExport.js';
export * from './withTimestamp.js';
export * from './eventContext.js';
export * from './eventStopPropagationFlag.js';
export * from './isDefined.js';
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 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,36 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { describe, expect, test } from 'vitest';
import { timestampedImageFileName } from './timestampedImageFileName.js';
describe('timestampedImageFileName', () => {
test('should append the timestamp and the extension', () => {
const name = timestampedImageFileName({ fileName: 'Chart', format: 'PNG', transparent: false });
expect(name).toMatch(/^Chart \d{4}-\d{2}-\d{2} \d{2}-\d{2}-\d{2}\.png$/);
});
test('should lowercase the extension per format', () => {
const name = timestampedImageFileName({ fileName: 'Plan', format: 'SVG', transparent: false });
expect(name.endsWith('.svg')).toBe(true);
});
test('should trim the requested name', () => {
const name = timestampedImageFileName({ fileName: ' Chart ', format: 'PNG', transparent: false });
expect(name.startsWith('Chart 20')).toBe(true);
});
test('should fall back when the requested name is blank', () => {
const name = timestampedImageFileName({ fileName: '', format: 'PNG', transparent: false });
expect(name.startsWith('image')).toBe(true);
});
});
@@ -0,0 +1,16 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { IImageExportOptions } from './IImageExportOptions.js';
import { withTimestamp } from './withTimestamp.js';
const FALLBACK_FILE_NAME = 'image';
/** `{ fileName: 'Chart', format: 'PNG' }` → `'Chart 2026-07-27 10-30-00.png'` */
export function timestampedImageFileName(options: IImageExportOptions): string {
return `${withTimestamp(options.fileName.trim() || FALLBACK_FILE_NAME)}.${options.format.toLowerCase()}`;
}
@@ -0,0 +1,15 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { blobToDataUrl } from './blobToDataUrl.js';
import type { ImageExportDataUrlReadyHandler, ImageExportReadyHandler } from './IImageExportOptions.js';
export function toDataUrlImageExport(handler: ImageExportDataUrlReadyHandler): ImageExportReadyHandler {
return async result => {
handler({ fileName: result.fileName, format: result.format, dataUrl: await blobToDataUrl(result.blob) });
};
}
@@ -1,12 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 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 withTimestamp(value: string) {
export function withTimestamp(value: string): string {
const now = new Date();
return `${value} ${now.toISOString().slice(0, 10)} ${('0' + now.getHours()).slice(-2)}-${('0' + now.getMinutes()).slice(-2)}-${(
'0' + now.getSeconds()
@@ -1,6 +1,11 @@
{
"extends": "@dbeaver/tsconfig/tsconfig.json",
"compilerOptions": {
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"rootDirs": [
+3 -3
View File
@@ -10,17 +10,17 @@
"resolutions": {
"lodash": ">4.17.23",
"immutable": ">=5.1.8",
"fast-uri": ">3.1.3",
"fast-uri": ">=4.1.2",
"glob": ">11.1.0",
"inflight": ">1.0.6",
"node-domexception": ">1.0.0",
"shell-quote": ">=1.9.0",
"esbuild": ">=0.28.1",
"form-data": ">=4.0.6",
"undici": ">=6.27.0",
"undici": ">=8.9.0",
"tar": ">=7.5.19",
"js-yaml": ">=5.2.2",
"brace-expansion": ">=5.0.8",
"brace-expansion": ">=5.0.9",
"axios": ">=1.18.0",
"postcss": ">=8.5.18"
},
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -92,7 +92,7 @@ export const ExportImageDialog = observer<DialogComponentProps<ExportImagePayloa
<Button type="button" variant="secondary" onClick={() => props.rejectDialog()}>
{translate('app_shared_inlineEditor_dialog_cancel')}
</Button>
<Button type="button" onClick={onExportHandler}>
<Button type="button" loader onClick={onExportHandler}>
{translate('ui_export')}
</Button>
</CommonDialogFooter>
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -20,4 +20,5 @@ export enum EObjectFeature {
'dataSourceConnected' = 'dataSourceConnected',
'entityContainer' = 'entityContainer',
'supportsDataFilter' = 'supportsDataFilter',
'supportsFullDdl' = 'supportsFullDdl', // some connections may not have the full DDL feature
}
@@ -0,0 +1,5 @@
mutation asyncSqlGenerateEntityQuery($generatorId: String!, $nodePathList: [String!]!, $generatorOptions: SQLQueryGeneratorOptions) {
taskInfo: asyncSqlGenerateEntityQuery(generatorId: $generatorId, nodePathList: $nodePathList, generatorOptions: $generatorOptions) {
...AsyncTaskInfo
}
}
@@ -1,5 +1,8 @@
query authLogout($provider: ID, $configuration: ID) {
result: authLogoutExtended(provider: $provider, configuration: $configuration) {
redirectLinks
redirectLinks {
url
sameTabRedirect
}
}
}
@@ -1,3 +0,0 @@
query metadataGetNodeDDL( $nodeId: ID! ) {
metadataGetNodeDDL( nodeId: $nodeId )
}
@@ -1,3 +0,0 @@
query sqlGenerateEntityQuery($generatorId: String!, $nodePathList: [String!]!, $generatorOptions: SQLQueryGeneratorOptions) {
sqlGenerateEntityQuery(generatorId: $generatorId, nodePathList: $nodePathList, generatorOptions: $generatorOptions)
}
@@ -1,52 +0,0 @@
/*
* 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 { beforeEach, describe, expect, it, vitest } from 'vitest';
import { download } from './download.js';
describe('download', () => {
beforeEach(() => {
// Clean up any existing links from previous tests
document.body.innerHTML = '';
});
it('should not create links after download', () => {
const blob = new Blob(['test'], { type: 'text/plain' });
const linksBefore = document.querySelectorAll('a');
expect(linksBefore.length).toBe(0);
download(blob, 'test.txt');
const linksAfter = document.querySelectorAll('a');
expect(linksAfter.length).toBe(0);
});
it('should create a link with url and click it to download file', () => {
const element = document.createElement('a');
const createElementSpy = vitest.spyOn(document, 'createElement').mockImplementation(() => element);
const clickSpy = vitest.spyOn(element, 'click');
const createObjectURLSpy = vitest.spyOn(URL, 'createObjectURL');
const blob = new Blob(['test'], { type: 'text/plain' });
download(blob, 'test.txt');
expect(createElementSpy).toHaveBeenCalledWith('a');
expect(createElementSpy).toHaveBeenCalledTimes(1);
expect(clickSpy).toHaveBeenCalledWith();
expect(clickSpy).toHaveBeenCalledTimes(1);
expect(createObjectURLSpy).toHaveBeenCalledWith(blob);
expect(createObjectURLSpy).toHaveBeenCalledTimes(1);
createElementSpy.mockRestore();
clickSpy.mockRestore();
createObjectURLSpy.mockRestore();
});
});
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 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,8 +8,7 @@
import * as modernScreenshot from 'modern-screenshot';
import type { Options } from 'modern-screenshot';
import { download } from './download.js';
import { isImageBroken } from './isBrokenImage.js';
import { download, isImageBroken } from '@dbeaver/js-helpers';
export type { Options as IScreenshotOptions };
-2
View File
@@ -62,7 +62,6 @@ export * from './getUniqueName.js';
export * from './isMapsEqual.js';
export * from './isObjectsEqual.js';
export * from './openCenteredPopup.js';
export * from './download.js';
export * from './downloadFromURL.js';
export * from './getTextFileReadingProcess.js';
export * from './getTextBetween.js';
@@ -81,7 +80,6 @@ export * from './removeMetadataFromDataURL.js';
export * from './removeLineBreak.js';
export * from './replaceSubstring.js';
export * from './formatNumber.js';
export * from './withTimestamp.js';
export * from './toSafeHtmlString.js';
export * from './getProgressPercent.js';
export * from './types/UndefinedToNull.js';
@@ -1,77 +0,0 @@
/*
* 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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { isImageBroken } from './isBrokenImage.js';
describe('isImageBroken', () => {
const OriginalImage = globalThis.Image;
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
if (OriginalImage) {
globalThis.Image = OriginalImage;
} else {
delete (globalThis as Record<string, unknown>)['Image'];
}
vi.restoreAllMocks();
vi.useRealTimers();
});
it('should return false if image loads successfully', async () => {
const srcSpy = vi.fn();
class MockImage {
public onload: (() => void) | null = null;
public onerror: (() => void) | null = null;
set src(value: string) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisRef = this;
srcSpy(value);
setTimeout(() => {
thisRef.onload?.();
}, 0);
vi.runAllTimers();
}
}
vi.stubGlobal('Image', MockImage);
await expect(isImageBroken('https://example.com/ok.png')).resolves.toBe(false);
expect(srcSpy).toHaveBeenCalledWith('https://example.com/ok.png');
});
it('should return true if image loading fails', async () => {
const srcSpy = vi.fn();
class MockImage {
public onload: (() => void) | null = null;
public onerror: (() => void) | null = null;
set src(value: string) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisRef = this;
srcSpy(value);
setTimeout(() => {
thisRef.onerror?.();
}, 0);
vi.runAllTimers();
}
}
vi.stubGlobal('Image', MockImage);
await expect(isImageBroken('https://example.com/broken.png')).resolves.toBe(true);
expect(srcSpy).toHaveBeenCalledWith('https://example.com/broken.png');
});
});
@@ -1,30 +0,0 @@
/*
* 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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { withTimestamp } from './withTimestamp.js';
describe('withTimestamp', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should generate a value with timestamp at the end', () => {
const mockDate = new Date('2020-09-09T14:13:20');
vi.setSystemTime(mockDate);
const value = 'value';
const expectedValue = `${value} 2020-09-09 14-13-20`;
expect(withTimestamp(value)).toEqual(expectedValue);
});
});
@@ -0,0 +1,13 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11514_20)">
<path d="M19 13.42H21V4.41998H19V13.42Z" fill="#3387C5"/>
<path d="M23.54 8.36L20 4.83L16.46 8.36L15.05 6.95L20 2L24.95 6.95L23.54 8.36Z" fill="#3387C5"/>
<path d="M21 23.42L12 14.42L3.71004 22.71L2.29004 21.3L12 11.59L21 20.59L25 16.59L29.71 21.3L28.29 22.71L25 19.42L21 23.42Z" fill="#3387C5"/>
<path d="M25 4L27 6H28V26H4V6H13L15 4H2V28H30V4H25Z" fill="#3387C5"/>
</g>
<defs>
<clipPath id="clip0_11514_20">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 614 B

@@ -0,0 +1,13 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11514_2)">
<path d="M14.3572 10.166H15.9286V2.94231H14.3572V10.166Z" fill="#3387C5"/>
<path d="M17.9244 6.10471L15.1429 3.27143L12.3615 6.10471L11.2537 4.973L15.1429 1L19.0322 4.973L17.9244 6.10471Z" fill="#3387C5"/>
<path d="M15.9286 18.5214L8.85719 11.2977L2.34362 17.9515L1.22791 16.8198L8.85719 9.02631L15.9286 16.2499L19.0715 13.0394L22.7722 16.8198L21.6565 17.9515L19.0715 15.3109L15.9286 18.5214Z" fill="#3387C5"/>
<path d="M19.0714 2.93439L20.6429 4.53964H21.4286V21.3948H2.57143V4.53964H9.64286L11.2143 2.93439H1V23.0001H23V2.93439H19.0714Z" fill="#3387C5"/>
</g>
<defs>
<clipPath id="clip0_11514_2">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 802 B

@@ -0,0 +1,13 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11514_11)">
<path d="M9.5 6.83289H10.5V2.23602H9.5V6.83289Z" fill="#3387C5"/>
<path d="M11.77 4.24845L10 2.44546L8.23002 4.24845L7.52502 3.52827L10 1L12.475 3.52827L11.77 4.24845Z" fill="#3387C5"/>
<path d="M10.5 12.15L6.00002 7.55312L1.85502 11.7873L1.14502 11.0672L6.00002 6.10767L10.5 10.7045L12.5 8.66148L14.855 11.0672L14.145 11.7873L12.5 10.1069L10.5 12.15Z" fill="#3387C5"/>
<path d="M12.5 2.23096L13.5 3.25248H14V13.9785H2V3.25248H6.5L7.5 2.23096H1V15H15V2.23096H12.5Z" fill="#3387C5"/>
</g>
<defs>
<clipPath id="clip0_11514_11">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 730 B

@@ -42,15 +42,22 @@ export const AIProfileFormPanel = observer<IAIProfileFormProps>(function AIProfi
},
});
const label = translate(creating ? 'plugin_ai_administration_profile_create' : 'plugin_ai_administration_profile_edit');
return (
<Form context={form} contents>
<TabsState container={aiProfileFormService.parts} localState={formState.parts} formState={formState}>
<div className="tw:flex tw:flex-col tw:flex-1 tw:h-full tw:overflow-auto theme-background-secondary theme-text-on-secondary">
<div
aria-label={label}
className="tw:flex tw:flex-col tw:flex-1 tw:h-full tw:overflow-auto theme-background-secondary theme-text-on-secondary"
>
<div className="tw:relative tw:flex tw:items-end tw:border-b-2 theme-border-color-background theme-background-secondary theme-text-on-secondary">
<div className="tw:flex-1 tw:overflow-hidden tw:pt-2 tw:pl-2">
<GroupTitle header>
<GroupBack onClick={() => aiProfileFormService.close()}>
<Text truncate>{formState.state.name || translate('ui_stepper_back')}</Text>
<Text truncate>
{creating ? translate('plugin_ai_administration_profile_create') : `${translate('ui_edit')} "${formState.state.name}"`}
</Text>
</GroupBack>
</GroupTitle>
<div className="tw:p-2">
@@ -55,7 +55,7 @@ export const AIProfilesPanel = observer<Props>(function AIProfilesPanel({ formSt
const profiles = profilesLoader.data.filter(isDefined);
const defaultProfileId = settingsInfoPart.initialState.defaultConfiguration;
const selection = useTableSelection(profiles.map(p => p.id));
const selection = useTableSelection(profiles.filter(p => p.id !== defaultProfileId).map(p => p.id));
const table = useAIProfilesTable(selection);
return (
@@ -51,8 +51,16 @@ export const AIProfilesTable = observer<Props>(function AIProfilesTable({ profil
return null;
}
const isDefault = profile.id === defaultProfileId;
if (column.key === SELECT_COLUMN.key) {
return <TableRowSelect id={profile.id} />;
return (
<TableRowSelect
id={profile.id}
disabled={isDefault}
title={isDefault ? translate('plugin_ai_administration_profile_default_delete_info') : undefined}
/>
);
}
if (column.key === NAME_COLUMN.key) {
@@ -65,7 +73,7 @@ export const AIProfilesTable = observer<Props>(function AIProfilesTable({ profil
onClick={() => aiProfileFormService.open(profile.id, profile.name)}
>
<Link truncate>{profile.name}</Link>
{profile.id === defaultProfileId && (
{isDefault && (
<span className="tw:text-xs tw:opacity-60 tw:whitespace-nowrap">{translate('plugin_ai_administration_profile_default_badge')}</span>
)}
</Command>
@@ -43,4 +43,10 @@ export default [
['plugin_ai_administration_profile_form_tab_options', 'Profil'],
['plugin_ai_administration_profile_name_max_length', 'Der Profilname darf {arg:length} Zeichen nicht überschreiten'],
['plugin_ai_administration_profile_name_min_length', 'Der Profilname muss mindestens {arg:length} Zeichen lang sein'],
[
'plugin_ai_administration_profile_default_delete_info',
'Das Standardprofil kann nicht gelöscht werden. Bitte wählen Sie ein anderes Profil als Standard aus, bevor Sie dieses Profil löschen.',
],
['plugin_ai_administration_profile_create', 'Neues Profil'],
['plugin_ai_administration_profile_edit', 'Profil bearbeiten'],
];
@@ -43,4 +43,10 @@ export default [
['plugin_ai_administration_profile_form_tab_options', 'Profile'],
['plugin_ai_administration_profile_name_max_length', 'Profile name must not exceed {arg:length} characters'],
['plugin_ai_administration_profile_name_min_length', 'Profile name must be at least {arg:length} characters'],
[
'plugin_ai_administration_profile_default_delete_info',
'The default profile cannot be deleted. Please select another profile as the default before deleting this profile.',
],
['plugin_ai_administration_profile_create', 'New Profile'],
['plugin_ai_administration_profile_edit', 'Edit Profile'],
];
@@ -43,4 +43,10 @@ export default [
['plugin_ai_administration_profile_form_tab_options', 'Profil'],
['plugin_ai_administration_profile_name_max_length', 'Le nom du profil ne doit pas dépasser {arg:length} caractères'],
['plugin_ai_administration_profile_name_min_length', 'Le nom du profil doit contenir au moins {arg:length} caractères'],
[
'plugin_ai_administration_profile_default_delete_info',
'Le profil par défaut ne peut pas être supprimé. Veuillez sélectionner un autre profil comme profil par défaut avant de supprimer ce profil.',
],
['plugin_ai_administration_profile_create', 'Nouveau profil'],
['plugin_ai_administration_profile_edit', 'Modifier le profil'],
];
@@ -43,4 +43,10 @@ export default [
['plugin_ai_administration_profile_form_tab_options', 'Профиль'],
['plugin_ai_administration_profile_name_max_length', 'Название профиля не должно превышать {arg:length} символов'],
['plugin_ai_administration_profile_name_min_length', 'Название профиля должно содержать не менее {arg:length} символов'],
[
'plugin_ai_administration_profile_default_delete_info',
'Профиль по умолчанию нельзя удалить. Перед удалением этого профиля выберите другой профиль по умолчанию.',
],
['plugin_ai_administration_profile_create', 'Новый профиль'],
['plugin_ai_administration_profile_edit', 'Редактировать профиль'],
];
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@ import { Filter, Group, IconOrImage, Loader, s, useS, useTranslate } from '@clou
import styles from './UsersTableFilters.module.css';
import { UsersTableFiltersDetails } from './UsersTableFiltersDetails.js';
import { type IUserFilters } from './useUsersTableFilters.js';
import { IconButton } from '@dbeaver/ui-kit';
interface Props {
filters: IUserFilters;
@@ -34,9 +35,14 @@ export const UsersTableFilters = observer<Props>(function UsersTableFilters({ fi
onChange={filters.setSearch}
/>
<div className={s(style, { actions: true })}>
<div className={s(style, { button: true, buttonActive: open })} onClick={() => setOpen(!open)}>
<IconButton
className={s(style, { button: true, buttonActive: open })}
size="small"
aria-label={translate('authentication_administration_users_filters_filter_label')}
onClick={() => setOpen(!open)}
>
<IconOrImage className={s(style, { iconOrImage: true })} icon="filter" />
</div>
</IconButton>
</div>
</div>
@@ -35,6 +35,7 @@ export default [
['authentication_administration_users_filters_status_enabled', 'ENABLED'],
['authentication_administration_users_filters_status_disabled', 'DISABLED'],
['authentication_administration_users_filters_status_all', 'ALL'],
['authentication_administration_users_filters_filter_label', 'Filter'],
['authentication_administration_users_empty', 'There are no users'],
['authentication_administration_users_delete_user', 'Delete user'],
['authentication_administration_users_delete_user_fail', 'Failed to delete user'],
@@ -35,6 +35,7 @@ export default [
['authentication_administration_users_filters_status_enabled', 'ACTIVÉ'],
['authentication_administration_users_filters_status_disabled', 'DÉSACTIVÉ'],
['authentication_administration_users_filters_status_all', 'TOUS'],
['authentication_administration_users_filters_filter_label', 'Filter'],
['authentication_administration_users_empty', "Il n'y a pas d'utilisateurs"],
['authentication_administration_users_delete_user', "Supprimer l'utilisateur"],
['authentication_administration_users_delete_user_fail', "Échec de la suppression de l'utilisateur"],
@@ -31,6 +31,7 @@ export default [
['authentication_administration_users_filters_status_enabled', 'ENABLED'],
['authentication_administration_users_filters_status_disabled', 'DISABLED'],
['authentication_administration_users_filters_status_all', 'ALL'],
['authentication_administration_users_filters_filter_label', 'Filter'],
['authentication_administration_users_empty', 'There are no users'],
['authentication_administration_users_delete_user', 'Delete user'],
['authentication_administration_users_delete_user_fail', 'Failed to delete user'],
@@ -35,6 +35,7 @@ export default [
['authentication_administration_users_filters_status_enabled', 'ВКЛЮЧЕН'],
['authentication_administration_users_filters_status_disabled', 'ВЫКЛЮЧЕН'],
['authentication_administration_users_filters_status_all', 'ВСЕ'],
['authentication_administration_users_filters_filter_label', 'Фильтр'],
['authentication_administration_users_empty', 'Нет пользователей'],
['authentication_administration_users_delete_user', 'Удалить пользователя'],
['authentication_administration_users_delete_user_fail', 'Не удалось удалить пользователя'],
@@ -35,6 +35,7 @@ export default [
['authentication_administration_users_filters_status_enabled', 'BẬT'],
['authentication_administration_users_filters_status_disabled', 'TẮT'],
['authentication_administration_users_filters_status_all', 'TẤT CẢ'],
['authentication_administration_users_filters_filter_label', 'Filter'],
['authentication_administration_users_empty', 'Không có người dùng nào'],
['authentication_administration_users_delete_user', 'Xóa người dùng'],
['authentication_administration_users_delete_user_fail', 'Xóa người dùng thất bại'],
@@ -34,6 +34,7 @@ export default [
['authentication_administration_users_filters_status_enabled', '启用'],
['authentication_administration_users_filters_status_disabled', '禁用'],
['authentication_administration_users_filters_status_all', '全部'],
['authentication_administration_users_filters_filter_label', 'Filter'],
['authentication_administration_users_empty', '无用户'],
['authentication_administration_users_delete_user', '删除用户'],
['authentication_administration_users_delete_user_fail', '删除用户失败'],
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -125,11 +125,17 @@ export class AuthenticationService extends Bootstrap {
const redirectLinks = userLogoutInfo.redirectLinks;
if (redirectLinks.length) {
const url = redirectLinks[0];
const redirectLink = redirectLinks[0]!;
if (redirectLink.sameTabRedirect) {
window.location.replace(redirectLink.url);
return;
}
const id = `okta-logout-id-${uuid()}`;
const popup = this.windowsService.open(id, {
url,
url: redirectLink.url,
target: id,
width: 600,
height: 700,
@@ -3,11 +3,19 @@
&.cm-tooltip-autocomplete {
& > ul {
border-radius: var(--theme-form-element-radius);
width: 320px;
}
& li {
display: flex;
align-items: center;
overflow: hidden;
& .cm-completionLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
background: var(--theme-sub-secondary);
@@ -38,6 +38,7 @@
"@cloudbeaver/plugin-data-viewer": "workspace:*",
"@cloudbeaver/plugin-navigation-tree": "workspace:*",
"@cloudbeaver/plugin-sql-editor": "workspace:*",
"@dbeaver/js-helpers": "workspace:*",
"mobx": "^6",
"mobx-react-lite": "^4",
"react": "^19",
@@ -11,7 +11,7 @@ import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { DATA_CONTEXT_NAV_NODE, EObjectFeature } from '@cloudbeaver/core-navigation-tree';
import { withTimestamp } from '@cloudbeaver/core-utils';
import { withTimestamp } from '@dbeaver/js-helpers';
import { ACTION_EXPORT, ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view';
import {
DATA_CONTEXT_DV_DDM,
@@ -10,6 +10,9 @@
{
"path": "../../common-typescript/@dbeaver/cli"
},
{
"path": "../../common-typescript/@dbeaver/js-helpers"
},
{
"path": "../core-blocks"
},
@@ -15,6 +15,7 @@ import { TableSelectionContext } from './TableSelectionContext.js';
interface BaseProps {
disabled?: boolean;
title?: string;
}
export interface ITableRowRootSelectProps extends BaseProps {
@@ -29,7 +30,7 @@ export interface ITableRowSelectProps extends BaseProps {
const CELL_CLASS_NAME = 'tw:flex tw:w-full tw:h-full tw:items-center tw:justify-center';
export const TableRowSelect = observer<ITableRowRootSelectProps | ITableRowSelectProps>(function TableRowSelect({ isRoot, id, disabled }) {
export const TableRowSelect = observer<ITableRowRootSelectProps | ITableRowSelectProps>(function TableRowSelect({ isRoot, id, disabled, title }) {
const selection = use(TableSelectionContext);
const translate = useTranslate();
@@ -47,6 +48,7 @@ export const TableRowSelect = observer<ITableRowRootSelectProps | ITableRowSelec
className={CELL_CLASS_NAME}
aria-label={translate('ui_select_all')}
tabIndex={0}
title={title}
checked={checked}
indeterminate={indeterminate}
disabled={rootDisabled}
@@ -61,6 +63,7 @@ export const TableRowSelect = observer<ITableRowRootSelectProps | ITableRowSelec
<Checkbox
className={CELL_CLASS_NAME}
aria-label={translate('plugin_data_grid_table_row_select')}
title={title}
tabIndex={0}
checked={checked}
disabled={disabled}
@@ -179,6 +179,7 @@ export class DataGridContextMenuGenerateSqlService {
query,
nodeId: nodePath ?? connectionId,
nodeName: this.getEntityNameFromNodePath(containerNodePath, model.name),
generatorId,
generatorName: this.localizationService.translate(generatorLabel),
options: getDefaultQueryGeneratorOptions(),
regenerateQuery: options =>
@@ -234,6 +235,7 @@ export class DataGridContextMenuGenerateSqlService {
query,
nodeId: nodePathList,
nodeName: this.getEntityNameFromNodePath(nodePathList, model.name),
generatorId: createGenerator.id,
generatorName: createGenerator.label,
options: getDefaultQueryGeneratorOptions(),
regenerateQuery: genOptions => this.sqlGenerationResource.generateEntityQuery(createGenerator.id, nodePathList, genOptions),
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,8 +10,8 @@ import { makeObservable, observable } from 'mobx';
import { QuotasService, ServerResourceQuotasResource } from '@cloudbeaver/core-root';
import { GraphQLService, ResultDataFormat } from '@cloudbeaver/core-sdk';
import { isResultSetContentValue } from '@dbeaver/result-set-api';
import { bytesToSize, download, downloadFromURL, GlobalConstants } from '@cloudbeaver/core-utils';
import { isNotNullDefined } from '@dbeaver/js-helpers';
import { bytesToSize, downloadFromURL, GlobalConstants } from '@cloudbeaver/core-utils';
import { download, isNotNullDefined } from '@dbeaver/js-helpers';
import { DatabaseDataAction } from '../../DatabaseDataAction.js';
import { IDatabaseDataSource } from '../../IDatabaseDataSource.js';
@@ -12,7 +12,8 @@ import { promptForFiles } from '@cloudbeaver/core-browser';
import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { download, getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils';
import { getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils';
import { download } from '@dbeaver/js-helpers';
import { isResultSetBinaryValue } from '@dbeaver/result-set-api';
import { createResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.js';
@@ -25,6 +25,8 @@
"@cloudbeaver/core-connections": "workspace:*",
"@cloudbeaver/core-data-context": "workspace:*",
"@cloudbeaver/core-di": "workspace:*",
"@cloudbeaver/core-events": "workspace:*",
"@cloudbeaver/core-localization": "workspace:*",
"@cloudbeaver/core-navigation-tree": "workspace:*",
"@cloudbeaver/core-resource": "workspace:*",
"@cloudbeaver/core-sdk": "workspace:*",
@@ -36,6 +38,8 @@
"@cloudbeaver/plugin-sql-editor": "workspace:*",
"@cloudbeaver/plugin-sql-editor-codemirror": "workspace:*",
"@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*",
"@cloudbeaver/plugin-sql-generator": "workspace:*",
"@dbeaver/js-helpers": "workspace:*",
"mobx": "^6",
"mobx-react-lite": "^4",
"react": "^19",
@@ -0,0 +1,14 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { createAction } from '@cloudbeaver/core-view';
export const ACTION_DDL_VIEWER_FULL_DDL = createAction('ddl-viewer-full-ddl', {
label: 'plugin_ddl_viewer_full_ddl',
tooltip: 'plugin_ddl_viewer_full_ddl',
type: 'checkbox',
});
@@ -1,10 +1,10 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 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 } from '@cloudbeaver/core-data-context';
export const DATA_CONTEXT_DDL_VIEWER_VALUE = createDataContext<string>('ddl-value');
export const DATA_CONTEXT_DDL_VIEWER_QUERY = createDataContext<string>('ddl-query');
@@ -1,42 +1,69 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 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, createConnectionParam } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { download, withTimestamp } from '@cloudbeaver/core-utils';
import { EObjectFeature, NavNodeInfoResource, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { MetadataMap } from '@cloudbeaver/core-utils';
import { download, withTimestamp } from '@dbeaver/js-helpers';
import { ACTION_SAVE, ActionService, MenuService } from '@cloudbeaver/core-view';
import { DDL_GENERATOR_ID, SqlGeneratorsResource } from '@cloudbeaver/plugin-sql-generator';
import { LocalStorageSqlDataSource } from '@cloudbeaver/plugin-sql-editor';
import { ACTION_SQL_EDITOR_OPEN, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
import { ACTION_DDL_VIEWER_FULL_DDL } from './ACTION_DDL_VIEWER_FULL_DDL.js';
import { DATA_CONTEXT_DDL_VIEWER_NODE } from './DATA_CONTEXT_DDL_VIEWER_NODE.js';
import { DATA_CONTEXT_DDL_VIEWER_VALUE } from './DATA_CONTEXT_DDL_VIEWER_VALUE.js';
import { DATA_CONTEXT_DDL_VIEWER_QUERY } from './DATA_CONTEXT_DDL_VIEWER_QUERY.js';
import { MENU_DDL_VIEWER_FOOTER } from './MENU_DDL_VIEWER_FOOTER.js';
@injectable(() => [NavNodeManagerService, ActionService, MenuService, SqlEditorNavigatorService, ConnectionInfoResource])
@injectable(() => [
NavNodeManagerService,
ActionService,
MenuService,
SqlEditorNavigatorService,
ConnectionInfoResource,
NavNodeInfoResource,
SqlGeneratorsResource,
])
export class DDLViewerFooterService {
private readonly fullDdlState = new MetadataMap<string, boolean>(() => false);
constructor(
private readonly navNodeManagerService: NavNodeManagerService,
private readonly actionsService: ActionService,
private readonly menuService: MenuService,
private readonly sqlEditorNavigatorService: SqlEditorNavigatorService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly sqlGeneratorsResource: SqlGeneratorsResource,
) {}
isFullDdlEnabled(nodeId: string): boolean {
return this.fullDdlState.get(nodeId);
}
setFullDdlEnabled(nodeId: string, value: boolean): void {
this.fullDdlState.set(nodeId, value);
}
toggleFullDdl(nodeId: string): void {
this.setFullDdlEnabled(nodeId, !this.isFullDdlEnabled(nodeId));
}
register(): void {
this.actionsService.addHandler({
id: 'ddl-viewer-footer-base-handler',
menus: [MENU_DDL_VIEWER_FOOTER],
contexts: [DATA_CONTEXT_DDL_VIEWER_NODE, DATA_CONTEXT_DDL_VIEWER_VALUE],
contexts: [DATA_CONTEXT_DDL_VIEWER_NODE, DATA_CONTEXT_DDL_VIEWER_QUERY],
actions: [ACTION_SAVE, ACTION_SQL_EDITOR_OPEN],
handler: async (context, action) => {
switch (action) {
case ACTION_SAVE: {
const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE)!;
const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_QUERY)!;
const nodeId = context.get(DATA_CONTEXT_DDL_VIEWER_NODE);
const blob = new Blob([ddl], {
@@ -50,7 +77,7 @@ export class DDLViewerFooterService {
break;
}
case ACTION_SQL_EDITOR_OPEN: {
const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE)!;
const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_QUERY)!;
const nodeId = context.get(DATA_CONTEXT_DDL_VIEWER_NODE)!;
const connection = this.connectionInfoResource.getConnectionForNode(nodeId);
@@ -92,5 +119,34 @@ export class DDLViewerFooterService {
menus: [MENU_DDL_VIEWER_FOOTER],
getItems: (context, items) => [...items, ACTION_SAVE, ACTION_SQL_EDITOR_OPEN],
});
this.menuService.addCreator({
menus: [MENU_DDL_VIEWER_FOOTER],
contexts: [DATA_CONTEXT_DDL_VIEWER_NODE],
isApplicable: context => this.isFullDDLSupported(context.get(DATA_CONTEXT_DDL_VIEWER_NODE)!),
getItems: (context, items) => [...items, ACTION_DDL_VIEWER_FULL_DDL],
});
this.actionsService.addHandler({
id: 'ddl-viewer-footer-full-ddl-handler',
menus: [MENU_DDL_VIEWER_FOOTER],
actions: [ACTION_DDL_VIEWER_FULL_DDL],
contexts: [DATA_CONTEXT_DDL_VIEWER_NODE],
isChecked: context => this.isFullDdlEnabled(context.get(DATA_CONTEXT_DDL_VIEWER_NODE)!),
handler: (context, action) => {
if (action === ACTION_DDL_VIEWER_FULL_DDL) {
this.toggleFullDdl(context.get(DATA_CONTEXT_DDL_VIEWER_NODE)!);
}
},
});
}
private isFullDDLSupported(nodeId: string): boolean {
const supportsFullDdl = this.navNodeInfoResource.get(nodeId)?.objectFeatures.includes(EObjectFeature.supportsFullDdl) ?? false;
const ddlGenerator = this.sqlGeneratorsResource
.get(nodeId)
?.find(generator => generator.id.toLowerCase().includes(DDL_GENERATOR_ID.toLowerCase()));
return supportsFullDdl && !!ddlGenerator;
}
}
@@ -7,7 +7,7 @@
*/
import { observer } from 'mobx-react-lite';
import { s, useResource, useS } from '@cloudbeaver/core-blocks';
import { s, useAutoLoad, useResource, useS } from '@cloudbeaver/core-blocks';
import {
ConnectionDialectResource,
ConnectionInfoActiveProjectKey,
@@ -22,17 +22,15 @@ import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigati
import { SQLCodeEditor, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-codemirror';
import { DATA_CONTEXT_DDL_VIEWER_NODE } from './DATA_CONTEXT_DDL_VIEWER_NODE.js';
import { DATA_CONTEXT_DDL_VIEWER_VALUE } from './DATA_CONTEXT_DDL_VIEWER_VALUE.js';
import { DdlResource } from './DdlResource.js';
import { DATA_CONTEXT_DDL_VIEWER_QUERY } from './DATA_CONTEXT_DDL_VIEWER_QUERY.js';
import style from './DDLViewerTabPanel.module.css';
import { MENU_DDL_VIEWER_FOOTER } from './MENU_DDL_VIEWER_FOOTER.js';
import { useDdlEntityQuery } from './useDdlEntityQuery.js';
export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(function DDLViewerTabPanel({ nodeId, folderId }) {
const styles = useS(style, MenuBarStyles, MenuBarItemStyles, MenuBarGroupStyles);
const menu = useMenu({ menu: MENU_DDL_VIEWER_FOOTER });
const ddlResource = useResource(DDLViewerTabPanel, DdlResource, nodeId);
const connectionInfoResource = useResource(DDLViewerTabPanel, ConnectionInfoResource, ConnectionInfoActiveProjectKey);
const connection = connectionInfoResource.resource.getConnectionForNode(nodeId);
const connectionParam = connection ? createConnectionParam(connection) : null;
@@ -42,16 +40,21 @@ export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(functio
if (sqlDialect) {
extensions.set(...sqlDialect);
}
const ddlData = ddlResource.data;
const ddlQueryState = useDdlEntityQuery(nodeId);
useAutoLoad(DDLViewerTabPanel, ddlQueryState);
const query = ddlQueryState.query ?? '';
useDataContextLink(menu.context, (context, id) => {
context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId, id);
context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, ddlData, id);
context.set(DATA_CONTEXT_DDL_VIEWER_QUERY, query, id);
});
return (
<div className={s(styles, { wrapper: true })}>
<SQLCodeEditor className={s(styles, { sqlCodeEditorLoader: true })} value={ddlResource.data ?? ''} extensions={extensions} readonly />
<SQLCodeEditor className={s(styles, { sqlCodeEditorLoader: true })} value={query} extensions={extensions} readonly />
<MenuBar className={s(styles, { menuBar: true, floating: true, withLabel: true })} menu={menu} compact={false} />
</div>
);
@@ -1,47 +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 { injectable } from '@cloudbeaver/core-di';
import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { CachedMapResource, isResourceAlias, type ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource';
import { GraphQLService } from '@cloudbeaver/core-sdk';
@injectable(() => [GraphQLService, NavNodeInfoResource])
export class DdlResource extends CachedMapResource<string, string> {
constructor(
private readonly graphQLService: GraphQLService,
private readonly navNodeInfoResource: NavNodeInfoResource,
) {
super();
this.navNodeInfoResource.outdateResource(this);
this.navNodeInfoResource.deleteInResource(this);
}
protected async loader(key: ResourceKey<string>): Promise<Map<string, string>> {
if (isResourceAlias(key)) {
throw new Error('Aliases not supported by this resource.');
}
const values: string[] = [];
await ResourceKeyUtils.forEachAsync(key, async nodeId => {
const { metadataGetNodeDDL } = await this.graphQLService.sdk.metadataGetNodeDDL({ nodeId });
if (metadataGetNodeDDL) {
values.push(metadataGetNodeDDL);
}
});
this.set(ResourceKeyUtils.toList(key), values);
return this.data;
}
protected validateKey(key: string): boolean {
return typeof key === 'string';
}
}
@@ -0,0 +1,99 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 } from 'mobx';
import { useObservableRef, useResource } from '@cloudbeaver/core-blocks';
import type { ILoadableState } from '@cloudbeaver/core-utils';
import { DDL_GENERATOR_ID, getDefaultQueryGeneratorOptions, SqlGeneratorsResource } from '@cloudbeaver/plugin-sql-generator';
import { useService } from '@cloudbeaver/core-di';
import { DDLViewerFooterService } from './DDLViewerFooterService.js';
interface IState extends ILoadableState {
query: string | null;
exception: Error | null;
promise: Promise<string> | null;
key: string | null;
nodeId: string;
sqlGeneratorsResource: SqlGeneratorsResource;
ddlGeneratorId: string | null;
ddlViewerFooterService: DDLViewerFooterService;
getCurrentKey: () => string | null;
}
export function useDdlEntityQuery(nodeId: string): IState {
const ddlViewerFooterService = useService(DDLViewerFooterService);
const sqlGeneratorsResource = useResource(useDdlEntityQuery, SqlGeneratorsResource, nodeId);
const ddlGenerator = sqlGeneratorsResource.data?.find(generator => generator.id.toLowerCase().includes(DDL_GENERATOR_ID.toLowerCase()));
const ddlGeneratorId = ddlGenerator?.id ?? null;
const state = useObservableRef<IState>(
() => ({
query: null,
exception: null,
promise: null,
key: null,
isLoadable() {
return this.ddlGeneratorId !== null;
},
isLoaded() {
return this.query !== null;
},
isError() {
return this.exception !== null;
},
isLoading() {
return this.promise !== null;
},
isOutdated() {
return this.getCurrentKey() !== this.key;
},
getCurrentKey() {
if (this.ddlGeneratorId === null) {
return null;
}
const isFullDdl = this.ddlViewerFooterService.isFullDdlEnabled(this.nodeId);
return `${this.nodeId}::${this.ddlGeneratorId}::${isFullDdl}`;
},
async load() {
if (this.ddlGeneratorId === null) {
return;
}
const key = this.getCurrentKey();
const isFullDdl = this.ddlViewerFooterService.isFullDdlEnabled(this.nodeId);
try {
this.exception = null;
this.promise = this.sqlGeneratorsResource.generateEntityQuery(this.ddlGeneratorId, this.nodeId, {
...getDefaultQueryGeneratorOptions(),
showFullDdl: isFullDdl,
});
this.query = await this.promise;
this.key = key;
} catch (exception: any) {
this.exception = exception;
} finally {
this.promise = null;
}
},
}),
{
query: observable.ref,
exception: observable.ref,
promise: observable.ref,
key: observable.ref,
nodeId: observable.ref,
ddlGeneratorId: observable.ref,
},
{ nodeId, ddlGeneratorId, sqlGeneratorsResource: sqlGeneratorsResource.resource, ddlViewerFooterService },
);
return state;
}
@@ -22,7 +22,7 @@ import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigati
import { SQLCodeEditor, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-codemirror';
import { DATA_CONTEXT_DDL_VIEWER_NODE } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_NODE.js';
import { DATA_CONTEXT_DDL_VIEWER_VALUE } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.js';
import { DATA_CONTEXT_DDL_VIEWER_QUERY } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_QUERY.js';
import style from '../DdlViewer/DDLViewerTabPanel.module.css';
import { MENU_DDL_VIEWER_FOOTER } from '../DdlViewer/MENU_DDL_VIEWER_FOOTER.js';
import { ExtendedDDLResource } from './ExtendedDDLResource.js';
@@ -46,7 +46,7 @@ export const ExtendedDDLViewerTabPanel: NavNodeTransformViewComponent = observer
useDataContextLink(menu.context, (context, id) => {
context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId, id);
context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, extendedDDlData, id);
context.set(DATA_CONTEXT_DDL_VIEWER_QUERY, extendedDDlData, id);
});
return (
@@ -0,0 +1,29 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { LocalizationService } from '@cloudbeaver/core-localization';
@injectable(() => [LocalizationService])
export class LocaleService extends Bootstrap {
constructor(private readonly localizationService: LocalizationService) {
super();
}
override register(): void {
this.localizationService.addProvider(this.provider.bind(this));
}
private async provider(locale: string) {
switch (locale) {
case 'ru':
return (await import('./locales/ru.js')).default;
default:
return (await import('./locales/en.js')).default;
}
}
}
@@ -0,0 +1,4 @@
export default [
['plugin_ddl_viewer_full_ddl', 'Full DDL'],
['plugin_ddl_viewer_full_ddl_error_title', 'Error generating full DDL'],
];
@@ -0,0 +1,4 @@
export default [
['plugin_ddl_viewer_full_ddl', 'Полный DDL'],
['plugin_ddl_viewer_full_ddl_error_title', 'Ошибка генерации полного DDL'],
];
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 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,8 +9,8 @@
import { Bootstrap, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di';
import { ExtendedDDLResource } from './ExtendedDDLViewer/ExtendedDDLResource.js';
import { DdlViewerBootstrap } from './DdlViewerBootstrap.js';
import { DdlResource } from './DdlViewer/DdlResource.js';
import { DDLViewerFooterService } from './DdlViewer/DDLViewerFooterService.js';
import { LocaleService } from './LocaleService.js';
export default ModuleRegistry.add({
name: '@cloudbeaver/plugin-ddl-viewer',
@@ -18,10 +18,9 @@ export default ModuleRegistry.add({
configure: serviceCollection => {
serviceCollection
.addSingleton(Bootstrap, DdlViewerBootstrap)
.addSingleton(Bootstrap, LocaleService)
.addSingleton(Dependency, proxy(ExtendedDDLResource))
.addSingleton(Dependency, proxy(DdlResource))
.addSingleton(DDLViewerFooterService)
.addSingleton(ExtendedDDLResource)
.addSingleton(DdlResource);
.addSingleton(ExtendedDDLResource);
},
});
@@ -7,6 +7,9 @@
"composite": true
},
"references": [
{
"path": "../../common-typescript/@dbeaver/js-helpers"
},
{
"path": "../core-blocks"
},
@@ -22,6 +25,12 @@
{
"path": "../core-di"
},
{
"path": "../core-events"
},
{
"path": "../core-localization"
},
{
"path": "../core-navigation-tree"
},
@@ -54,6 +63,9 @@
},
{
"path": "../plugin-sql-editor-navigation-tab"
},
{
"path": "../plugin-sql-generator"
}
],
"include": [
@@ -51,6 +51,13 @@ export class SettingsAdministrationPluginBootstrap extends Bootstrap {
return true;
},
onDeActivate: () => {
this.settingsAdministrationService.tabsContainer.getDisplayed().forEach(tab => {
if (tab.options?.source.isEdited()) {
tab.options?.source.clear();
}
});
},
getContentComponent: () => SettingsAdministration,
getDrawerComponent: () => SettingsDrawerItem,
});
@@ -28,6 +28,18 @@ export function useSqlDialectAutocompletion(data: ISQLEditorData): [Compartment,
const localizationService = useService(LocalizationService);
const optionsRef = useObjectRef({ data });
function fuzzyFilterProposals(proposals: SQLProposal[], word: string): SQLProposal[] {
const matchedResults = new Set(
fuzzyMatch({
query: word,
items: proposals,
fields: ['displayString', 'replacementString'],
}).map(r => r.item),
);
return proposals.filter(proposal => matchedResults.has(proposal));
}
const config = useMemo<CompletionConfig>(() => {
if (!codemirror) {
return undefined;
@@ -44,35 +56,24 @@ export function useSqlDialectAutocompletion(data: ISQLEditorData): [Compartment,
if (word === '*') {
filteredProposals = proposals;
} else {
const matchResults = fuzzyMatch({
query: word,
items: proposals.filter(
({ replacementString, displayString }) =>
sanitizeProposal(displayString) !== wordLowerCase && replacementString.toLocaleLowerCase() !== wordLowerCase,
),
fields: ['displayString', 'replacementString'],
filteredProposals = fuzzyFilterProposals(proposals, word).sort((a, b) => {
const aDisplayLower = sanitizeProposal(a.displayString).toLocaleLowerCase();
const aReplacementLower = a.replacementString.toLocaleLowerCase();
const bDisplayLower = sanitizeProposal(b.displayString).toLocaleLowerCase();
const bReplacementLower = b.replacementString.toLocaleLowerCase();
const aIncludes = aDisplayLower.includes(wordLowerCase) || aReplacementLower.includes(wordLowerCase);
const bIncludes = bDisplayLower.includes(wordLowerCase) || bReplacementLower.includes(wordLowerCase);
if (aIncludes && !bIncludes) {
return -1;
}
if (!aIncludes && bIncludes) {
return 1;
}
return (b.score ?? 0) - (a.score ?? 0);
});
filteredProposals = matchResults
.sort((a, b) => {
const aDisplayLower = sanitizeProposal(a.item.displayString).toLocaleLowerCase();
const aReplacementLower = a.item.replacementString.toLocaleLowerCase();
const bDisplayLower = sanitizeProposal(b.item.displayString).toLocaleLowerCase();
const bReplacementLower = b.item.replacementString.toLocaleLowerCase();
const aIncludes = aDisplayLower.includes(wordLowerCase) || aReplacementLower.includes(wordLowerCase);
const bIncludes = bDisplayLower.includes(wordLowerCase) || bReplacementLower.includes(wordLowerCase);
if (aIncludes && !bIncludes) {
return -1;
}
if (!aIncludes && bIncludes) {
return 1;
}
return (b.score ?? 0) - (a.score ?? 0);
})
.map(r => r.item);
}
if (filteredProposals.length === 0 && !hasSameName && explicit) {
@@ -11,7 +11,7 @@ import { InputField, Translate, useForm } from '@cloudbeaver/core-blocks';
import { ExportScriptDialogContext, type IScriptExportTabProps } from '@cloudbeaver/plugin-script-export';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import { downloadSql } from '../downloadSql.js';
import { withTimestamp } from '@cloudbeaver/core-utils';
import { withTimestamp } from '@dbeaver/js-helpers';
import { useState, useContext } from 'react';
export const LocalExportPanel: TabContainerPanelComponent<IScriptExportTabProps> = observer(function LocalExportPanel({
@@ -8,7 +8,8 @@
import type { IDataContextProvider } from '@cloudbeaver/core-data-context';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { WindowEventsService } from '@cloudbeaver/core-root';
import { getTextFileReadingProcess, throttle, withTimestamp } from '@cloudbeaver/core-utils';
import { getTextFileReadingProcess, throttle } from '@cloudbeaver/core-utils';
import { withTimestamp } from '@dbeaver/js-helpers';
import {
ACTION_DOWNLOAD,
ACTION_REDO,
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { download } from '@cloudbeaver/core-utils';
import { download } from '@dbeaver/js-helpers';
function sanitizeFilename(filename: string): string {
// Remove or replace invalid filesystem characters
@@ -30,6 +30,7 @@
"@cloudbeaver/core-localization": "workspace:*",
"@cloudbeaver/core-navigation-tree": "workspace:*",
"@cloudbeaver/core-resource": "workspace:*",
"@cloudbeaver/core-root": "workspace:*",
"@cloudbeaver/core-sdk": "workspace:*",
"@cloudbeaver/core-utils": "workspace:*",
"@cloudbeaver/core-view": "workspace:*",
@@ -38,6 +39,7 @@
"@cloudbeaver/plugin-sql-editor": "workspace:*",
"@cloudbeaver/plugin-sql-editor-codemirror": "workspace:*",
"@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*",
"@dbeaver/js-helpers": "workspace:*",
"mobx": "^6",
"mobx-react-lite": "^4",
"react": "^19",
@@ -23,8 +23,8 @@ import {
import { ConnectionDialectResource, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import type { DialogComponentProps } from '@cloudbeaver/core-dialogs';
import { download, withTimestamp } from '@cloudbeaver/core-utils';
import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { EObjectFeature, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { download, withTimestamp } from '@dbeaver/js-helpers';
import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
import { SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
import { SQLCodeEditor, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-codemirror';
@@ -33,9 +33,12 @@ import { action, observable } from 'mobx';
import { NotificationService } from '@cloudbeaver/core-events';
import type { SqlQueryGeneratorOptions } from '@cloudbeaver/core-sdk';
import { DDL_GENERATOR_ID } from './SqlGeneratorsResource.js';
interface Payload {
nodeId: string;
nodeName?: string;
generatorId: string;
generatorName?: string;
query: string;
options?: SqlQueryGeneratorOptions;
@@ -51,6 +54,7 @@ export const GeneratedSqlDialog = observer<DialogComponentProps<Payload>>(functi
() => ({
useFullyQualifiedNames: payload.options?.useFullyQualifiedNames ?? true,
compactSql: payload.options?.compactSql ?? false,
showFullDdl: payload.options?.showFullDdl,
query: payload.query,
loading: false,
handleOptionChange: async function handleOptionChange<T extends keyof typeof state>(key: T, value: (typeof state)[T]) {
@@ -61,6 +65,7 @@ export const GeneratedSqlDialog = observer<DialogComponentProps<Payload>>(functi
this.query = await this.payload.regenerateQuery({
compactSql: this.compactSql,
useFullyQualifiedNames: this.useFullyQualifiedNames,
showFullDdl: this.showFullDdl,
});
} catch (error: any) {
this.notificationService.logException(error, 'app_shared_sql_generators_error_title');
@@ -73,6 +78,7 @@ export const GeneratedSqlDialog = observer<DialogComponentProps<Payload>>(functi
handleOptionChange: action.bound,
useFullyQualifiedNames: observable.ref,
compactSql: observable.ref,
showFullDdl: observable.ref,
query: observable.ref,
loading: observable.ref,
},
@@ -96,6 +102,10 @@ export const GeneratedSqlDialog = observer<DialogComponentProps<Payload>>(functi
}
const visibleLoading = useStateDelay(state.loading, 300);
const isDdlGenerator = payload.generatorId.toLowerCase().includes(DDL_GENERATOR_ID.toLowerCase());
const node = navNodeManagerService.getNode(payload.nodeId);
const supportsFullDdl = node?.objectFeatures.includes(EObjectFeature.supportsFullDdl) ?? false;
const hasFullDdl = isDdlGenerator && supportsFullDdl;
async function handleOpenInEditor() {
try {
@@ -134,21 +144,24 @@ export const GeneratedSqlDialog = observer<DialogComponentProps<Payload>>(functi
<div className="tw:flex tw:flex-row tw:gap-3 tw:w-full">
<Checkbox
id="use-fully-qualified-names"
state={state}
name="useFullyQualifiedNames"
disabled={visibleLoading}
label={translate('app_shared_sql_generators_use_fully_qualified_names')}
onChange={value => state.handleOptionChange('useFullyQualifiedNames', value)}
/>
<Checkbox
id="compact-sql"
state={state}
name="compactSql"
disabled={visibleLoading}
label={translate('app_shared_sql_generators_compact_sql')}
onChange={value => state.handleOptionChange('compactSql', value)}
/>
{hasFullDdl && (
<Checkbox
id="show-full-ddl"
disabled={visibleLoading}
label={translate('app_shared_sql_generators_full_ddl')}
onChange={value => state.handleOptionChange('showFullDdl', value)}
/>
)}
</div>
</div>
<div className="tw:flex tw:items-center tw:justify-between tw:w-full tw:gap-6">
@@ -12,8 +12,8 @@ import { DATA_CONTEXT_NAV_NODE, EObjectFeature } from '@cloudbeaver/core-navigat
import { getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource';
import { MenuBaseItem, MenuService } from '@cloudbeaver/core-view';
import { MENU_SQL_GENERATORS } from './MENU_SQL_GENERATORS.js';
import { getDefaultQueryGeneratorOptions, SqlGeneratorsResource } from './SqlGeneratorsResource.js';
import { MENU_SQL_GENERATORS } from './MENU_SQL_GENERATORS.js';
import { NotificationService } from '@cloudbeaver/core-events';
const GeneratedSqlDialog = importLazyComponent(() => import('./GeneratedSqlDialog.js').then(m => m.GeneratedSqlDialog));
@@ -86,6 +86,7 @@ export class SqlGeneratorsBootstrap extends Bootstrap {
query,
nodeId: node.uri,
nodeName: node.name,
generatorId: action.id,
generatorName: action.label,
options: getDefaultQueryGeneratorOptions(),
regenerateQuery: options => this.sqlGeneratorsResource.generateEntityQuery(action.id, node.uri, options),
@@ -8,6 +8,7 @@
import { injectable } from '@cloudbeaver/core-di';
import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
import { AsyncTaskInfoService } from '@cloudbeaver/core-root';
import {
GraphQLService,
type SqlGenerateResultSetQueryQueryVariables,
@@ -19,13 +20,15 @@ export const MAX_GENERATORS_LENGTH = 15;
export const getDefaultQueryGeneratorOptions = (): SqlQueryGeneratorOptions => ({
useFullyQualifiedNames: true,
compactSql: false,
showFullDdl: false,
});
export const DDL_GENERATOR_ID = 'tableDDL';
@injectable(() => [GraphQLService, NavNodeInfoResource])
@injectable(() => [GraphQLService, AsyncTaskInfoService, NavNodeInfoResource])
export class SqlGeneratorsResource extends CachedMapResource<string, SqlQueryGenerator[]> {
constructor(
private readonly graphQLService: GraphQLService,
private readonly asyncTaskInfoService: AsyncTaskInfoService,
private readonly navNodeInfoResource: NavNodeInfoResource,
) {
super();
@@ -35,13 +38,20 @@ export class SqlGeneratorsResource extends CachedMapResource<string, SqlQueryGen
}
async generateEntityQuery(generatorId: string, nodePathList: string | string[], options?: SqlQueryGeneratorOptions): Promise<string> {
const result = await this.graphQLService.sdk.sqlGenerateEntityQuery({
generatorId,
nodePathList,
generatorOptions: options,
const task = this.asyncTaskInfoService.create(async () => {
const { taskInfo } = await this.graphQLService.sdk.asyncSqlGenerateEntityQuery({
generatorId,
nodePathList,
generatorOptions: options,
});
return taskInfo;
});
return result.sqlGenerateEntityQuery;
const info = await this.asyncTaskInfoService.run(task);
await this.asyncTaskInfoService.remove(task.id);
return info.taskResult;
}
async generateResultSetSql(params: SqlGenerateResultSetQueryQueryVariables): Promise<string | null> {
@@ -2,6 +2,7 @@ export default [
['app_shared_sql_generators_error_title', 'SQL Generation Error'],
['app_shared_sql_generators_use_fully_qualified_names', 'Use fully qualified names'],
['app_shared_sql_generators_compact_sql', 'Compact SQL'],
['app_shared_sql_generators_full_ddl', 'Full DDL'],
['app_shared_sql_generators_open_in_editor', 'Open in SQL Editor'],
['app_shared_sql_generators_error_open_editor', 'Error opening SQL Editor'],
];
@@ -2,6 +2,7 @@ export default [
['app_shared_sql_generators_error_title', 'Erreur de génération SQL'],
['app_shared_sql_generators_use_fully_qualified_names', 'Utiliser les noms complets'],
['app_shared_sql_generators_compact_sql', 'SQL compact'],
['app_shared_sql_generators_full_ddl', 'Full DDL'],
['app_shared_sql_generators_open_in_editor', "Ouvrir dans l'éditeur SQL"],
['app_shared_sql_generators_error_open_editor', "Erreur lors de l'ouverture de l'éditeur SQL"],
];
@@ -2,6 +2,7 @@ export default [
['app_shared_sql_generators_error_title', 'Errore di generazione SQL'],
['app_shared_sql_generators_use_fully_qualified_names', 'Usa nomi completi'],
['app_shared_sql_generators_compact_sql', 'SQL compatto'],
['app_shared_sql_generators_full_ddl', 'Full DDL'],
['app_shared_sql_generators_open_in_editor', "Apri nell'editor SQL"],
['app_shared_sql_generators_error_open_editor', "Errore durante l'apertura dell'editor SQL"],
];
@@ -2,6 +2,7 @@ export default [
['app_shared_sql_generators_error_title', 'Ошибка генерации SQL'],
['app_shared_sql_generators_use_fully_qualified_names', 'Использовать полные имена'],
['app_shared_sql_generators_compact_sql', 'Компактный SQL'],
['app_shared_sql_generators_full_ddl', 'Полный DDL'],
['app_shared_sql_generators_open_in_editor', 'Открыть в SQL редакторе'],
['app_shared_sql_generators_error_open_editor', 'Ошибка при открытии SQL редактора'],
];
@@ -2,6 +2,7 @@ export default [
['app_shared_sql_generators_error_title', 'Lỗi tạo SQL'],
['app_shared_sql_generators_use_fully_qualified_names', 'Sử dụng tên đầy đủ'],
['app_shared_sql_generators_compact_sql', 'SQL gọn'],
['app_shared_sql_generators_full_ddl', 'Full DDL'],
['app_shared_sql_generators_open_in_editor', 'Mở trong trình soạn thảo SQL'],
['app_shared_sql_generators_error_open_editor', 'Lỗi khi mở trình soạn thảo SQL'],
];
@@ -2,6 +2,7 @@ export default [
['app_shared_sql_generators_error_title', 'SQL生成错误'],
['app_shared_sql_generators_use_fully_qualified_names', '使用完全限定名'],
['app_shared_sql_generators_compact_sql', '紧凑SQL'],
['app_shared_sql_generators_full_ddl', 'Full DDL'],
['app_shared_sql_generators_open_in_editor', '在SQL编辑器中打开'],
['app_shared_sql_generators_error_open_editor', '打开SQL编辑器时出错'],
];
@@ -10,6 +10,9 @@
{
"path": "../../common-typescript/@dbeaver/cli"
},
{
"path": "../../common-typescript/@dbeaver/js-helpers"
},
{
"path": "../core-blocks"
},
@@ -37,6 +40,9 @@
{
"path": "../core-resource"
},
{
"path": "../core-root"
},
{
"path": "../core-sdk"
},
+27 -12
View File
@@ -3110,6 +3110,7 @@ __metadata:
"@cloudbeaver/plugin-sql-editor": "workspace:*"
"@cloudbeaver/tsconfig": "workspace:*"
"@dbeaver/cli": "workspace:*"
"@dbeaver/js-helpers": "workspace:*"
"@types/react": "npm:^19"
mobx: "npm:^6"
mobx-react-lite: "npm:^4"
@@ -3503,6 +3504,8 @@ __metadata:
"@cloudbeaver/core-connections": "workspace:*"
"@cloudbeaver/core-data-context": "workspace:*"
"@cloudbeaver/core-di": "workspace:*"
"@cloudbeaver/core-events": "workspace:*"
"@cloudbeaver/core-localization": "workspace:*"
"@cloudbeaver/core-navigation-tree": "workspace:*"
"@cloudbeaver/core-resource": "workspace:*"
"@cloudbeaver/core-sdk": "workspace:*"
@@ -3514,7 +3517,9 @@ __metadata:
"@cloudbeaver/plugin-sql-editor": "workspace:*"
"@cloudbeaver/plugin-sql-editor-codemirror": "workspace:*"
"@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*"
"@cloudbeaver/plugin-sql-generator": "workspace:*"
"@cloudbeaver/tsconfig": "workspace:*"
"@dbeaver/js-helpers": "workspace:*"
"@types/react": "npm:^19"
mobx: "npm:^6"
mobx-react-lite: "npm:^4"
@@ -4717,6 +4722,7 @@ __metadata:
"@cloudbeaver/core-localization": "workspace:*"
"@cloudbeaver/core-navigation-tree": "workspace:*"
"@cloudbeaver/core-resource": "workspace:*"
"@cloudbeaver/core-root": "workspace:*"
"@cloudbeaver/core-sdk": "workspace:*"
"@cloudbeaver/core-utils": "workspace:*"
"@cloudbeaver/core-view": "workspace:*"
@@ -4727,6 +4733,7 @@ __metadata:
"@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*"
"@cloudbeaver/tsconfig": "workspace:*"
"@dbeaver/cli": "workspace:*"
"@dbeaver/js-helpers": "workspace:*"
"@types/react": "npm:^19"
mobx: "npm:^6"
mobx-react-lite: "npm:^4"
@@ -5363,6 +5370,7 @@ __metadata:
"@dbeaver/tsconfig": "workspace:^"
async-mutex: "npm:^0"
color: "npm:^5.0.2"
modern-screenshot: "npm:^4"
p-debounce: "npm:^5"
p-memoize: "npm:^8.0.0"
quick-lru: "npm:^7.1.0"
@@ -10508,12 +10516,12 @@ __metadata:
languageName: node
linkType: hard
"brace-expansion@npm:>=5.0.8":
version: 5.0.8
resolution: "brace-expansion@npm:5.0.8"
"brace-expansion@npm:>=5.0.9":
version: 5.0.9
resolution: "brace-expansion@npm:5.0.9"
dependencies:
balanced-match: "npm:^4.0.2"
checksum: 10c0/73304caafd00fdc5b0168e693ac15bf25b6357dec76d1195fcd628fe2cf99c46f9c889a7ecfa3cfe236dbd2d5ce3e27a6ccc4370b46d475d0d19081e11f86019
checksum: 10c0/3dea38884a1c3c8b1c9c44a7402a0c76fca460f70cffb3127242b0b4cbf4472019e022ade021eec44838ff19f1dac2625dfd11dd459d7e1e055b0698a8d52fec
languageName: node
linkType: hard
@@ -12910,10 +12918,10 @@ __metadata:
languageName: node
linkType: hard
"fast-uri@npm:>3.1.3":
version: 4.1.1
resolution: "fast-uri@npm:4.1.1"
checksum: 10c0/669e12d1e7c3ba931b93d94def9a0a7352288503f09479f62e31b7bd731fe73f9b62ff411724e2d452c39a57d756cdce4169ea6d1b80424c2c254c1eecde10e5
"fast-uri@npm:>=4.1.2":
version: 4.1.2
resolution: "fast-uri@npm:4.1.2"
checksum: 10c0/539fda19fbc7ada89ac8c5beca3c7ab57d643789485ca25f2b88f94edaea3fc90d553ecd55d85b95d5c819ab2bf57f6ff7880aa0a92d0883e25bd35f30860e6a
languageName: node
linkType: hard
@@ -16099,6 +16107,13 @@ __metadata:
languageName: node
linkType: hard
"modern-screenshot@npm:^4":
version: 4.7.0
resolution: "modern-screenshot@npm:4.7.0"
checksum: 10c0/9c5fbe4f4c73d3dcf0ba7fb76c1671ad148fdedffc84700e0b8ca1c1640a6ab1930718ae1424b0a6517b98d8f18c898ea0e62ee0ca57a25bfce7b914df721a63
languageName: node
linkType: hard
"ms@npm:^2.1.3":
version: 2.1.3
resolution: "ms@npm:2.1.3"
@@ -19521,10 +19536,10 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:>=6.27.0":
version: 8.5.0
resolution: "undici@npm:8.5.0"
checksum: 10c0/7c25fd96a4dcc607b905995f6feec948f74a6759cadbea4c6eb9402517aab4887987c8001c80d482297d04f975f44a383325b89577ac21e587983ed088408065
"undici@npm:>=8.9.0":
version: 8.10.0
resolution: "undici@npm:8.10.0"
checksum: 10c0/37ae2b1db8f65c3a003504e2040e96887b99f9db16ba07dcf49c37ed8b7f8c72f4452f2d46f52f7c9e49518fe8325e3b5e7e02ac3a2424886bd67d4b62a0ecab
languageName: node
linkType: hard