diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index d98767370d..ae0e96b251 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -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"); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java index 98b72f6ea7..5bef8b9698 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java @@ -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; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebRegistryConstant.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebRegistryConstant.java index 31c06abb49..d5801e32a4 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebRegistryConstant.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebRegistryConstant.java @@ -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"; diff --git a/server/bundles/io.cloudbeaver.product.ce/plugin.xml b/server/bundles/io.cloudbeaver.product.ce/plugin.xml index 9b505a93b0..0e39a3a7c3 100644 --- a/server/bundles/io.cloudbeaver.product.ce/plugin.xml +++ b/server/bundles/io.cloudbeaver.product.ce/plugin.xml @@ -17,7 +17,7 @@ - + diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index ef4158a089..5498775af0 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -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 { diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutInfo.java index 9e8aaefb3d..31f63d4689 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutInfo.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutInfo.java @@ -20,5 +20,5 @@ import org.jkiss.code.NotNull; import java.util.List; -public record WebLogoutInfo(@NotNull List redirectLinks) { +public record WebLogoutInfo(@NotNull List redirectLinks) { } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutLink.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutLink.java new file mode 100644 index 0000000000..dbe96d6bf3 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutLink.java @@ -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) { +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index b5496e8804..6600de981a 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -249,7 +249,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth { List removedInfos = webSession.removeAuthInfo(providerId); var cbApp = CBApplication.getInstance(); - List logoutUrls = new ArrayList<>(); + List 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); } diff --git a/server/bundles/io.cloudbeaver.service.security/plugin.xml b/server/bundles/io.cloudbeaver.service.security/plugin.xml index 4ebb57d2c7..e29aa46c07 100644 --- a/server/bundles/io.cloudbeaver.service.security/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.security/plugin.xml @@ -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"> diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java index df0827b755..895e01c591 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java @@ -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 + } } }"""; diff --git a/webapp/common-typescript/@dbeaver/js-helpers/package.json b/webapp/common-typescript/@dbeaver/js-helpers/package.json index 7ff6753137..07c0777fb1 100644 --- a/webapp/common-typescript/@dbeaver/js-helpers/package.json +++ b/webapp/common-typescript/@dbeaver/js-helpers/package.json @@ -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", diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/IImageExportOptions.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/IImageExportOptions.ts new file mode 100644 index 0000000000..c0b502a416 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/IImageExportOptions.ts @@ -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; + +/** 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; diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/blobToDataUrl.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/blobToDataUrl.ts new file mode 100644 index 0000000000..d93fbe0ff2 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/blobToDataUrl.ts @@ -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 { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(blob); + }); +} diff --git a/webapp/packages/core-utils/src/download.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/download.ts similarity index 94% rename from webapp/packages/core-utils/src/download.ts rename to webapp/common-typescript/@dbeaver/js-helpers/src/download.ts index 31516cbf1f..c86979801c 100644 --- a/webapp/packages/core-utils/src/download.ts +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/download.ts @@ -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. diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/downloadImageExport.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/downloadImageExport.ts new file mode 100644 index 0000000000..23298666a6 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/downloadImageExport.ts @@ -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); +} diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/exportDomImage.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/exportDomImage.ts new file mode 100644 index 0000000000..993f1b7462 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/exportDomImage.ts @@ -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; + /** 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 { + 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 { + return domToBlob(node, { ...screenshotOptions, type: 'image/png' }); +} + +async function renderSvg(node: Element, screenshotOptions: Options): Promise { + const svg = await domToForeignObjectSvg(node, screenshotOptions); + const svgData = new XMLSerializer().serializeToString(svg); + + return new Blob(['\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 }; +} diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/fitToPixelBudget.test.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/fitToPixelBudget.test.ts new file mode 100644 index 0000000000..eb33fc16c7 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/fitToPixelBudget.test.ts @@ -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); + }); +}); diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/fitToPixelBudget.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/fitToPixelBudget.ts new file mode 100644 index 0000000000..9722eb2709 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/fitToPixelBudget.ts @@ -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)); +} diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts index ba96d752a7..fc570bf4c9 100644 --- a/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/index.ts @@ -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'; diff --git a/webapp/packages/core-utils/src/isBrokenImage.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/isImageBroken.ts similarity index 89% rename from webapp/packages/core-utils/src/isBrokenImage.ts rename to webapp/common-typescript/@dbeaver/js-helpers/src/isImageBroken.ts index 28c6f4e70c..049f2ceb54 100644 --- a/webapp/packages/core-utils/src/isBrokenImage.ts +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/isImageBroken.ts @@ -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. diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/timestampedImageFileName.test.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/timestampedImageFileName.test.ts new file mode 100644 index 0000000000..4d4c3264e3 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/timestampedImageFileName.test.ts @@ -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); + }); +}); diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/timestampedImageFileName.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/timestampedImageFileName.ts new file mode 100644 index 0000000000..d8c5adaeb6 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/timestampedImageFileName.ts @@ -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()}`; +} diff --git a/webapp/common-typescript/@dbeaver/js-helpers/src/toDataUrlImageExport.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/toDataUrlImageExport.ts new file mode 100644 index 0000000000..9013e6d094 --- /dev/null +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/toDataUrlImageExport.ts @@ -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) }); + }; +} diff --git a/webapp/packages/core-utils/src/withTimestamp.ts b/webapp/common-typescript/@dbeaver/js-helpers/src/withTimestamp.ts similarity index 77% rename from webapp/packages/core-utils/src/withTimestamp.ts rename to webapp/common-typescript/@dbeaver/js-helpers/src/withTimestamp.ts index 4858da1e1e..678372b819 100644 --- a/webapp/packages/core-utils/src/withTimestamp.ts +++ b/webapp/common-typescript/@dbeaver/js-helpers/src/withTimestamp.ts @@ -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() diff --git a/webapp/common-typescript/@dbeaver/js-helpers/tsconfig.json b/webapp/common-typescript/@dbeaver/js-helpers/tsconfig.json index 569071c38b..1bcc9398e0 100644 --- a/webapp/common-typescript/@dbeaver/js-helpers/tsconfig.json +++ b/webapp/common-typescript/@dbeaver/js-helpers/tsconfig.json @@ -1,6 +1,11 @@ { "extends": "@dbeaver/tsconfig/tsconfig.json", "compilerOptions": { + "lib": [ + "ESNext", + "DOM", + "DOM.Iterable" + ], "tsBuildInfoFile": "./lib/.tsbuildinfo", "rootDir": "src", "rootDirs": [ diff --git a/webapp/package.json b/webapp/package.json index 8eea27090f..092202e240 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -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" }, diff --git a/webapp/packages/core-blocks/src/ExportImageDialog/ExportImageDialog.tsx b/webapp/packages/core-blocks/src/ExportImageDialog/ExportImageDialog.tsx index 1cd14b8243..5f63ad043d 100644 --- a/webapp/packages/core-blocks/src/ExportImageDialog/ExportImageDialog.tsx +++ b/webapp/packages/core-blocks/src/ExportImageDialog/ExportImageDialog.tsx @@ -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 props.rejectDialog()}> {translate('app_shared_inlineEditor_dialog_cancel')} - diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/EObjectFeature.ts b/webapp/packages/core-navigation-tree/src/NodesManager/EObjectFeature.ts index 5208f2b4a0..7538c59897 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/EObjectFeature.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/EObjectFeature.ts @@ -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 } diff --git a/webapp/packages/core-sdk/src/queries/asyncSqlGenerateEntityQuery.gql b/webapp/packages/core-sdk/src/queries/asyncSqlGenerateEntityQuery.gql new file mode 100644 index 0000000000..3cc9c98b02 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/asyncSqlGenerateEntityQuery.gql @@ -0,0 +1,5 @@ +mutation asyncSqlGenerateEntityQuery($generatorId: String!, $nodePathList: [String!]!, $generatorOptions: SQLQueryGeneratorOptions) { + taskInfo: asyncSqlGenerateEntityQuery(generatorId: $generatorId, nodePathList: $nodePathList, generatorOptions: $generatorOptions) { + ...AsyncTaskInfo + } +} diff --git a/webapp/packages/core-sdk/src/queries/authentication/authLogout.gql b/webapp/packages/core-sdk/src/queries/authentication/authLogout.gql index 4997dbbd56..4a2121b9d5 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/authLogout.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/authLogout.gql @@ -1,5 +1,8 @@ query authLogout($provider: ID, $configuration: ID) { result: authLogoutExtended(provider: $provider, configuration: $configuration) { - redirectLinks + redirectLinks { + url + sameTabRedirect + } } } diff --git a/webapp/packages/core-sdk/src/queries/metadataGetNodeDDL.gql b/webapp/packages/core-sdk/src/queries/metadataGetNodeDDL.gql deleted file mode 100644 index eb2944a809..0000000000 --- a/webapp/packages/core-sdk/src/queries/metadataGetNodeDDL.gql +++ /dev/null @@ -1,3 +0,0 @@ -query metadataGetNodeDDL( $nodeId: ID! ) { - metadataGetNodeDDL( nodeId: $nodeId ) -} diff --git a/webapp/packages/core-sdk/src/queries/sqlGenerateEntityQuery.gql b/webapp/packages/core-sdk/src/queries/sqlGenerateEntityQuery.gql deleted file mode 100644 index aa437c9013..0000000000 --- a/webapp/packages/core-sdk/src/queries/sqlGenerateEntityQuery.gql +++ /dev/null @@ -1,3 +0,0 @@ -query sqlGenerateEntityQuery($generatorId: String!, $nodePathList: [String!]!, $generatorOptions: SQLQueryGeneratorOptions) { - sqlGenerateEntityQuery(generatorId: $generatorId, nodePathList: $nodePathList, generatorOptions: $generatorOptions) -} diff --git a/webapp/packages/core-utils/src/download.test.ts b/webapp/packages/core-utils/src/download.test.ts deleted file mode 100644 index e39e133698..0000000000 --- a/webapp/packages/core-utils/src/download.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/webapp/packages/core-utils/src/downloadImage.ts b/webapp/packages/core-utils/src/downloadImage.ts index a8d007b7dd..323688e3e4 100644 --- a/webapp/packages/core-utils/src/downloadImage.ts +++ b/webapp/packages/core-utils/src/downloadImage.ts @@ -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 }; diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index cfdd6140fa..04172e3524 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -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'; diff --git a/webapp/packages/core-utils/src/isBrokenImage.test.ts b/webapp/packages/core-utils/src/isBrokenImage.test.ts deleted file mode 100644 index 68772965ab..0000000000 --- a/webapp/packages/core-utils/src/isBrokenImage.test.ts +++ /dev/null @@ -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)['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'); - }); -}); diff --git a/webapp/packages/core-utils/src/withTimestamp.test.ts b/webapp/packages/core-utils/src/withTimestamp.test.ts deleted file mode 100644 index 01acc095e9..0000000000 --- a/webapp/packages/core-utils/src/withTimestamp.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/webapp/packages/core-view/public/icons/export_as_img.svg b/webapp/packages/core-view/public/icons/export_as_img.svg new file mode 100644 index 0000000000..17a5f6972a --- /dev/null +++ b/webapp/packages/core-view/public/icons/export_as_img.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/webapp/packages/core-view/public/icons/export_as_img_m.svg b/webapp/packages/core-view/public/icons/export_as_img_m.svg new file mode 100644 index 0000000000..7788f15687 --- /dev/null +++ b/webapp/packages/core-view/public/icons/export_as_img_m.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/webapp/packages/core-view/public/icons/export_as_img_sm.svg b/webapp/packages/core-view/public/icons/export_as_img_sm.svg new file mode 100644 index 0000000000..f1083f2605 --- /dev/null +++ b/webapp/packages/core-view/public/icons/export_as_img_sm.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx index a845a6062d..4fd82beff4 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx @@ -42,15 +42,22 @@ export const AIProfileFormPanel = observer(function AIProfi }, }); + const label = translate(creating ? 'plugin_ai_administration_profile_create' : 'plugin_ai_administration_profile_edit'); + return (
-
+
aiProfileFormService.close()}> - {formState.state.name || translate('ui_stepper_back')} + + {creating ? translate('plugin_ai_administration_profile_create') : `${translate('ui_edit')} "${formState.state.name}"`} +
diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx index 332d20f136..a7f4e9085b 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx @@ -55,7 +55,7 @@ export const AIProfilesPanel = observer(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 ( diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx index 1f2738a967..a3cb33e51b 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx @@ -51,8 +51,16 @@ export const AIProfilesTable = observer(function AIProfilesTable({ profil return null; } + const isDefault = profile.id === defaultProfileId; + if (column.key === SELECT_COLUMN.key) { - return ; + return ( + + ); } if (column.key === NAME_COLUMN.key) { @@ -65,7 +73,7 @@ export const AIProfilesTable = observer(function AIProfilesTable({ profil onClick={() => aiProfileFormService.open(profile.id, profile.name)} > {profile.name} - {profile.id === defaultProfileId && ( + {isDefault && ( {translate('plugin_ai_administration_profile_default_badge')} )} diff --git a/webapp/packages/plugin-ai-administration/src/locales/de.ts b/webapp/packages/plugin-ai-administration/src/locales/de.ts index c1706f0b63..51b3cbcdc2 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/de.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/de.ts @@ -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'], ]; diff --git a/webapp/packages/plugin-ai-administration/src/locales/en.ts b/webapp/packages/plugin-ai-administration/src/locales/en.ts index 972e9b704c..62ecac58bd 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/en.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/en.ts @@ -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'], ]; diff --git a/webapp/packages/plugin-ai-administration/src/locales/fr.ts b/webapp/packages/plugin-ai-administration/src/locales/fr.ts index 34872a4ff8..13787fd5b1 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/fr.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/fr.ts @@ -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'], ]; diff --git a/webapp/packages/plugin-ai-administration/src/locales/ru.ts b/webapp/packages/plugin-ai-administration/src/locales/ru.ts index f941d1aa0f..11eb9637ec 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/ru.ts @@ -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', 'Редактировать профиль'], ]; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx index de91f23469..e60c33e2f6 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx @@ -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(function UsersTableFilters({ fi onChange={filters.setSearch} />
-
setOpen(!open)}> + setOpen(!open)} + > -
+
diff --git a/webapp/packages/plugin-authentication-administration/src/locales/en.ts b/webapp/packages/plugin-authentication-administration/src/locales/en.ts index cf4efd5149..8abadfe0df 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/en.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/en.ts @@ -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'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/fr.ts b/webapp/packages/plugin-authentication-administration/src/locales/fr.ts index acda50ea60..0a9e9766fc 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/fr.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/fr.ts @@ -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"], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/it.ts b/webapp/packages/plugin-authentication-administration/src/locales/it.ts index 67b4ba1bf9..de87d2c4f9 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/it.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/it.ts @@ -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'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/ru.ts b/webapp/packages/plugin-authentication-administration/src/locales/ru.ts index 92f273d3da..0034d1e538 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/ru.ts @@ -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', 'Не удалось удалить пользователя'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/vi.ts b/webapp/packages/plugin-authentication-administration/src/locales/vi.ts index b9330130b1..b7e44aabe0 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/vi.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/vi.ts @@ -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'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/zh.ts b/webapp/packages/plugin-authentication-administration/src/locales/zh.ts index 3d16b47209..524fc3c479 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/zh.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/zh.ts @@ -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', '删除用户失败'], diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index d5db6fb7e3..a8e59ab428 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -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, diff --git a/webapp/packages/plugin-codemirror6/src/theme/base-code-editor-autocompletion.css b/webapp/packages/plugin-codemirror6/src/theme/base-code-editor-autocompletion.css index f80a707396..bc7f2c7478 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/base-code-editor-autocompletion.css +++ b/webapp/packages/plugin-codemirror6/src/theme/base-code-editor-autocompletion.css @@ -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); diff --git a/webapp/packages/plugin-data-export/package.json b/webapp/packages/plugin-data-export/package.json index 25d791dab5..66b56ca825 100644 --- a/webapp/packages/plugin-data-export/package.json +++ b/webapp/packages/plugin-data-export/package.json @@ -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", diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index d04c6abddd..2df3e42546 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -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, diff --git a/webapp/packages/plugin-data-export/tsconfig.json b/webapp/packages/plugin-data-export/tsconfig.json index b7ff6da332..5d34c6f85b 100644 --- a/webapp/packages/plugin-data-export/tsconfig.json +++ b/webapp/packages/plugin-data-export/tsconfig.json @@ -10,6 +10,9 @@ { "path": "../../common-typescript/@dbeaver/cli" }, + { + "path": "../../common-typescript/@dbeaver/js-helpers" + }, { "path": "../core-blocks" }, diff --git a/webapp/packages/plugin-data-grid/src/TableRowSelect.tsx b/webapp/packages/plugin-data-grid/src/TableRowSelect.tsx index a60fc2b649..496c439869 100644 --- a/webapp/packages/plugin-data-grid/src/TableRowSelect.tsx +++ b/webapp/packages/plugin-data-grid/src/TableRowSelect.tsx @@ -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(function TableRowSelect({ isRoot, id, disabled }) { +export const TableRowSelect = observer(function TableRowSelect({ isRoot, id, disabled, title }) { const selection = use(TableSelectionContext); const translate = useTranslate(); @@ -47,6 +48,7 @@ export const TableRowSelect = observer @@ -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), diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts index 203e1da56a..81ebfb1844 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts @@ -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'; diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts index a91ba4b18f..ce2158caf5 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts @@ -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'; diff --git a/webapp/packages/plugin-ddl-viewer/package.json b/webapp/packages/plugin-ddl-viewer/package.json index 8f882961a4..1925cc09b2 100644 --- a/webapp/packages/plugin-ddl-viewer/package.json +++ b/webapp/packages/plugin-ddl-viewer/package.json @@ -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", diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/ACTION_DDL_VIEWER_FULL_DDL.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/ACTION_DDL_VIEWER_FULL_DDL.ts new file mode 100644 index 0000000000..40fb969bcf --- /dev/null +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/ACTION_DDL_VIEWER_FULL_DDL.ts @@ -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', +}); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_QUERY.ts similarity index 63% rename from webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.ts rename to webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_QUERY.ts index ac2e51d766..caa900ab7a 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_QUERY.ts @@ -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('ddl-value'); +export const DATA_CONTEXT_DDL_VIEWER_QUERY = createDataContext('ddl-query'); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts index 8e4ead0170..6dcf8cb2c5 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts @@ -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(() => 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; } } diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx index 716f93861c..7eedf48bdf 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx @@ -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 (
- +
); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DdlResource.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DdlResource.ts deleted file mode 100644 index c6a8ecb43a..0000000000 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DdlResource.ts +++ /dev/null @@ -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 { - constructor( - private readonly graphQLService: GraphQLService, - private readonly navNodeInfoResource: NavNodeInfoResource, - ) { - super(); - - this.navNodeInfoResource.outdateResource(this); - this.navNodeInfoResource.deleteInResource(this); - } - - protected async loader(key: ResourceKey): Promise> { - 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'; - } -} diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/useDdlEntityQuery.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/useDdlEntityQuery.ts new file mode 100644 index 0000000000..9c154a6310 --- /dev/null +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/useDdlEntityQuery.ts @@ -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 | 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( + () => ({ + 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; +} diff --git a/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx b/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx index 466ece51a3..08fdde9428 100644 --- a/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx +++ b/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx @@ -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 ( diff --git a/webapp/packages/plugin-ddl-viewer/src/LocaleService.ts b/webapp/packages/plugin-ddl-viewer/src/LocaleService.ts new file mode 100644 index 0000000000..fae7357b77 --- /dev/null +++ b/webapp/packages/plugin-ddl-viewer/src/LocaleService.ts @@ -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; + } + } +} diff --git a/webapp/packages/plugin-ddl-viewer/src/locales/en.ts b/webapp/packages/plugin-ddl-viewer/src/locales/en.ts new file mode 100644 index 0000000000..a0d8e3ed2e --- /dev/null +++ b/webapp/packages/plugin-ddl-viewer/src/locales/en.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_ddl_viewer_full_ddl', 'Full DDL'], + ['plugin_ddl_viewer_full_ddl_error_title', 'Error generating full DDL'], +]; diff --git a/webapp/packages/plugin-ddl-viewer/src/locales/ru.ts b/webapp/packages/plugin-ddl-viewer/src/locales/ru.ts new file mode 100644 index 0000000000..3277e1a5d3 --- /dev/null +++ b/webapp/packages/plugin-ddl-viewer/src/locales/ru.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_ddl_viewer_full_ddl', 'Полный DDL'], + ['plugin_ddl_viewer_full_ddl_error_title', 'Ошибка генерации полного DDL'], +]; diff --git a/webapp/packages/plugin-ddl-viewer/src/module.ts b/webapp/packages/plugin-ddl-viewer/src/module.ts index 31511d19d0..c25c2b4e27 100644 --- a/webapp/packages/plugin-ddl-viewer/src/module.ts +++ b/webapp/packages/plugin-ddl-viewer/src/module.ts @@ -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); }, }); diff --git a/webapp/packages/plugin-ddl-viewer/tsconfig.json b/webapp/packages/plugin-ddl-viewer/tsconfig.json index 000492456e..0574b48ad7 100644 --- a/webapp/packages/plugin-ddl-viewer/tsconfig.json +++ b/webapp/packages/plugin-ddl-viewer/tsconfig.json @@ -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": [ diff --git a/webapp/packages/plugin-settings-administration/src/SettingsAdministrationPluginBootstrap.ts b/webapp/packages/plugin-settings-administration/src/SettingsAdministrationPluginBootstrap.ts index 649b0ad7d2..48283729e5 100644 --- a/webapp/packages/plugin-settings-administration/src/SettingsAdministrationPluginBootstrap.ts +++ b/webapp/packages/plugin-settings-administration/src/SettingsAdministrationPluginBootstrap.ts @@ -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, }); diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectAutocompletion.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectAutocompletion.ts index 62b52f58ac..a1ca62f287 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectAutocompletion.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectAutocompletion.ts @@ -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(() => { 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) { diff --git a/webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportPanel.tsx b/webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportPanel.tsx index 7fac98e8fb..21c13f7616 100644 --- a/webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportPanel.tsx +++ b/webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportPanel.tsx @@ -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 = observer(function LocalExportPanel({ diff --git a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts index beaebae84d..d83db74f68 100644 --- a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts @@ -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, diff --git a/webapp/packages/plugin-sql-editor/src/downloadSql.ts b/webapp/packages/plugin-sql-editor/src/downloadSql.ts index b9ffa82748..9d5e6c8295 100644 --- a/webapp/packages/plugin-sql-editor/src/downloadSql.ts +++ b/webapp/packages/plugin-sql-editor/src/downloadSql.ts @@ -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 diff --git a/webapp/packages/plugin-sql-generator/package.json b/webapp/packages/plugin-sql-generator/package.json index 92bd0f40de..57270f6b13 100644 --- a/webapp/packages/plugin-sql-generator/package.json +++ b/webapp/packages/plugin-sql-generator/package.json @@ -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", diff --git a/webapp/packages/plugin-sql-generator/src/SqlGenerators/GeneratedSqlDialog.tsx b/webapp/packages/plugin-sql-generator/src/SqlGenerators/GeneratedSqlDialog.tsx index e448a1061f..349bf317d2 100644 --- a/webapp/packages/plugin-sql-generator/src/SqlGenerators/GeneratedSqlDialog.tsx +++ b/webapp/packages/plugin-sql-generator/src/SqlGenerators/GeneratedSqlDialog.tsx @@ -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>(functi () => ({ useFullyQualifiedNames: payload.options?.useFullyQualifiedNames ?? true, compactSql: payload.options?.compactSql ?? false, + showFullDdl: payload.options?.showFullDdl, query: payload.query, loading: false, handleOptionChange: async function handleOptionChange(key: T, value: (typeof state)[T]) { @@ -61,6 +65,7 @@ export const GeneratedSqlDialog = observer>(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>(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>(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>(functi
state.handleOptionChange('useFullyQualifiedNames', value)} /> - state.handleOptionChange('compactSql', value)} /> + {hasFullDdl && ( + state.handleOptionChange('showFullDdl', value)} + /> + )}
diff --git a/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsBootstrap.ts b/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsBootstrap.ts index ee4d5385de..c24631afcb 100644 --- a/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsBootstrap.ts +++ b/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsBootstrap.ts @@ -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), diff --git a/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsResource.ts b/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsResource.ts index 95eaf63a12..a712535ef0 100644 --- a/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsResource.ts +++ b/webapp/packages/plugin-sql-generator/src/SqlGenerators/SqlGeneratorsResource.ts @@ -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 { constructor( private readonly graphQLService: GraphQLService, + private readonly asyncTaskInfoService: AsyncTaskInfoService, private readonly navNodeInfoResource: NavNodeInfoResource, ) { super(); @@ -35,13 +38,20 @@ export class SqlGeneratorsResource extends CachedMapResource { - 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 { diff --git a/webapp/packages/plugin-sql-generator/src/locales/en.ts b/webapp/packages/plugin-sql-generator/src/locales/en.ts index ee34546328..48c468ac3e 100644 --- a/webapp/packages/plugin-sql-generator/src/locales/en.ts +++ b/webapp/packages/plugin-sql-generator/src/locales/en.ts @@ -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'], ]; diff --git a/webapp/packages/plugin-sql-generator/src/locales/fr.ts b/webapp/packages/plugin-sql-generator/src/locales/fr.ts index b1877ca2c7..326376d22d 100644 --- a/webapp/packages/plugin-sql-generator/src/locales/fr.ts +++ b/webapp/packages/plugin-sql-generator/src/locales/fr.ts @@ -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"], ]; diff --git a/webapp/packages/plugin-sql-generator/src/locales/it.ts b/webapp/packages/plugin-sql-generator/src/locales/it.ts index b980aadd5a..50176cbdfc 100644 --- a/webapp/packages/plugin-sql-generator/src/locales/it.ts +++ b/webapp/packages/plugin-sql-generator/src/locales/it.ts @@ -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"], ]; diff --git a/webapp/packages/plugin-sql-generator/src/locales/ru.ts b/webapp/packages/plugin-sql-generator/src/locales/ru.ts index 8a44f1cefb..643f6c438e 100644 --- a/webapp/packages/plugin-sql-generator/src/locales/ru.ts +++ b/webapp/packages/plugin-sql-generator/src/locales/ru.ts @@ -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 редактора'], ]; diff --git a/webapp/packages/plugin-sql-generator/src/locales/vi.ts b/webapp/packages/plugin-sql-generator/src/locales/vi.ts index 41cbd4a09c..454f389678 100644 --- a/webapp/packages/plugin-sql-generator/src/locales/vi.ts +++ b/webapp/packages/plugin-sql-generator/src/locales/vi.ts @@ -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'], ]; diff --git a/webapp/packages/plugin-sql-generator/src/locales/zh.ts b/webapp/packages/plugin-sql-generator/src/locales/zh.ts index e40f345c8c..0eee5fc34e 100644 --- a/webapp/packages/plugin-sql-generator/src/locales/zh.ts +++ b/webapp/packages/plugin-sql-generator/src/locales/zh.ts @@ -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编辑器时出错'], ]; diff --git a/webapp/packages/plugin-sql-generator/tsconfig.json b/webapp/packages/plugin-sql-generator/tsconfig.json index b13d67548a..c7229863bd 100644 --- a/webapp/packages/plugin-sql-generator/tsconfig.json +++ b/webapp/packages/plugin-sql-generator/tsconfig.json @@ -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" }, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index e15e10472b..fc9976dce3 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -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