;
-
- tileId: number;
- caption: string;
- description: string;
- type: BrowserTileType;
- icon: string;
- hideActions?: boolean;
-
- onAction?: (action: string, props: IBrowserTileProperties, options: ITileActionOptions) => void;
- onTileReorder?: (draggedTileId: number, props: unknown) => void;
-}
-
-export abstract class BrowserTile extends ComponentBase
{
-
- private actionsRef = createRef();
-
- protected abstract renderTileActionUI: () => ComponentChild;
-
- public constructor(props: P) {
- super(props);
-
- this.addHandledProperties("innerRef", "tileId", "caption", "description", "type", "icon", "hideActions",
- "onAction", "onTileReorder");
- this.connectDragEvents();
- }
-
- public render(): ComponentChild {
- const { innerRef, tileId, type, icon, caption, description, hideActions } = this.props;
-
- const className = this.getEffectiveClassNames([
- "browserTile",
- this.classFromProperty(type === BrowserTileType.CreateNew, "secondary"),
- ]);
-
- return (
-
- );
- }
-
- protected override handleDragEvent = (type: DragEventType, e: DragEvent): boolean => {
- if (!e.dataTransfer) {
- return false;
- }
-
- const element = e.currentTarget as HTMLElement;
- switch (type) {
- case DragEventType.Start: {
- e.dataTransfer.effectAllowed = "move";
- e.dataTransfer.setData("browser/tile", element.id);
-
- return true;
- }
-
- case DragEventType.Enter: {
- e.stopPropagation();
- e.preventDefault();
-
- if (element.contains(e.relatedTarget as Node)) {
- element.classList.add("dropTarget");
- }
-
- return true;
- }
-
- case DragEventType.Leave: {
- e.stopPropagation();
- e.preventDefault();
-
- if (!element.contains(e.relatedTarget as Node)) {
- element.classList.remove("dropTarget");
- }
-
- return true;
- }
-
- case DragEventType.Drop: {
- element.classList.remove("dropTarget");
-
- const id = filterInt(e.dataTransfer.getData("browser/tile"));
- if (!isNaN(id)) {
- const { onTileReorder } = this.props;
- onTileReorder?.(id, this.props);
- }
-
- return true;
- }
-
- default: {
- return false;
- }
- }
-
- };
-
- private handleClick = (e: MouseEvent | KeyboardEvent): void => {
- const { onAction, type } = this.props;
-
- const event = e as MouseEvent;
- const button = event.currentTarget as HTMLButtonElement;
-
- // Have to prevent double clicks on browser tiles. But since everything is async there's no way to know
- // the action triggered by the tile is finished (or at least started, so the button is hidden).
- // Hence the only way to enable the button is to use a timer.
- button.disabled = true;
- setTimeout(() => {
- button.disabled = false;
- }, 200);
- e.stopPropagation();
-
- if (type === BrowserTileType.Open) {
- onAction?.("open", this.props, { newTab: event.metaKey || event.altKey });
- } else {
- onAction?.("new", this.props, {});
- }
- };
-
- private handleKeydown = (e: KeyboardEvent): void => {
- if (e.key === KeyboardKeys.A) { // Unmodified A key.
- e.stopPropagation();
- const { type, onAction } = this.props;
- if (type === BrowserTileType.Open) {
- this.actionsRef.current?.focus();
- onAction?.("menu", this.props, { target: this.actionsRef.current });
- }
- }
- };
-}
diff --git a/gui/frontend/src/components/ui/CodeEditor/CodeEditor.tsx b/gui/frontend/src/components/ui/CodeEditor/CodeEditor.tsx
index 5428cdc08..62ae39d88 100644
--- a/gui/frontend/src/components/ui/CodeEditor/CodeEditor.tsx
+++ b/gui/frontend/src/components/ui/CodeEditor/CodeEditor.tsx
@@ -26,18 +26,15 @@
import "./CodeEditor.css";
import { SymbolTable } from "antlr4-c3";
-import Color from "color";
import { ComponentChild, createRef } from "preact";
import "./userWorker.js";
import {
- CodeEditorMode, ICodeEditorViewState, IDisposable, ILanguageDefinition, IPosition, IProviderEditorModel,
+ CodeEditorMode, ICodeEditorViewState, IDisposable, IPosition, IProviderEditorModel,
IScriptExecutionOptions, KeyCode, KeyMod, languages, Monaco, Range, Selection,
} from "./index.js";
-import { msg } from "./languages/msg/msg.contribution.js";
-import { mysql } from "./languages/mysql/mysql.contribution.js";
import { ExecutionContexts } from "../../../script-execution/ExecutionContexts.js";
import { PresentationInterface } from "../../../script-execution/PresentationInterface.js";
@@ -50,22 +47,12 @@ import {
import { Settings } from "../../../supplement/Settings/Settings.js";
import { editorRangeToTextRange } from "../../../utilities/ts-helpers.js";
-import { IThemeChangeData, IThemeObject, ITokenEntry } from "../../Theming/ThemeManager.js";
import { MessageType } from "../../../app-logic/general-types.js";
import { ExecutionContext } from "../../../script-execution/ExecutionContext.js";
import { splitTextToLines } from "../../../utilities/string-helpers.js";
import { ComponentBase, IComponentProperties } from "../Component/ComponentBase.js";
import type { ICodeEditorOptions } from "../index.js";
-import { CodeCompletionProvider } from "./CodeCompletionProvider.js";
-import { DefinitionProvider } from "./DefinitionProvider.js";
-import { DocumentHighlightProvider } from "./DocumentHighlightProvider.js";
-import { FormattingProvider } from "./FormattingProvider.js";
-import { HoverProvider } from "./HoverProvider.js";
-import { MsgSemanticTokensProvider } from "./MsgSemanticTokensProvider.js";
-import { ReferencesProvider } from "./ReferencesProvider.js";
-import { RenameProvider } from "./RenameProvider.js";
-import { SignatureHelpProvider } from "./SignatureHelpProvider.js";
/** Used when splitting pasted text to find the individual language blocks. */
interface ITextBlockEntry {
@@ -242,110 +229,6 @@ export class CodeEditor extends ComponentBase {
}
}
- /**
- * Updates the theme used by all code editor instances.
- *
- * @param theme The theme name (DOM safe).
- * @param type The base type of the theme.
- * @param values The actual theme values.
- */
- public static updateTheme(theme: string, type: "light" | "dark", values: IThemeObject): void {
- Monaco.remeasureFonts();
-
- // Convert all color values to CSS hex form.
- const entries: { [key: string]: string; } = {};
- for (const [key, value] of Object.entries(values.colors || {})) {
- entries[key] = (new Color(value)).hexa();
- }
-
- const tokenRules: Monaco.ITokenThemeRule[] = [];
- (values.tokenColors || []).forEach((value: ITokenEntry): void => {
- const scopeValue = value.scope || [];
- const scopes = Array.isArray(scopeValue) ? scopeValue : scopeValue.split(",");
- scopes.forEach((scope: string): void => {
- tokenRules.push({
- token: scope,
- foreground: (new Color(value.settings.foreground)).hexa(),
- background: (new Color(value.settings.background)).hexa(),
- fontStyle: value.settings.fontStyle,
- });
- });
- });
-
- Monaco.defineTheme(theme, {
- base: type === "light" ? "vs" : "vs-dark",
- inherit: false,
- rules: tokenRules,
- colors: entries,
- });
-
- Monaco.setTheme(theme);
- }
-
- /**
- * Called once to initialize various aspects of the monaco-editor subsystem (like languages, themes, options etc.)
- */
- public static configureMonaco(): void {
- if (CodeEditor.monacoConfigured) {
- return;
- }
-
- CodeEditor.monacoConfigured = true;
-
- languages.onLanguage(msg.id, () => {
- void msg.loader().then((definition: ILanguageDefinition) => {
- languages.setMonarchTokensProvider(msg.id, definition.language);
- languages.setLanguageConfiguration(msg.id, definition.languageConfiguration);
- });
- });
-
- languages.onLanguage("mysql", () => {
- void mysql.loader().then((definition: ILanguageDefinition) => {
- languages.setMonarchTokensProvider("mysql", definition.language);
- languages.setLanguageConfiguration("mysql", definition.languageConfiguration);
- });
- });
-
- const editorLanguages = ["msg", "javascript", "typescript", "mysql", "python"];
- languages.registerDocumentSemanticTokensProvider(editorLanguages, new MsgSemanticTokensProvider());
- languages.registerCompletionItemProvider(editorLanguages, new CodeCompletionProvider());
- languages.registerHoverProvider(editorLanguages, new HoverProvider());
- languages.registerSignatureHelpProvider(editorLanguages, new SignatureHelpProvider());
- languages.registerDocumentHighlightProvider(editorLanguages, new DocumentHighlightProvider());
- languages.registerDefinitionProvider(editorLanguages, new DefinitionProvider());
- languages.registerReferenceProvider(editorLanguages, new ReferencesProvider());
- languages.registerDocumentFormattingEditProvider(editorLanguages, new FormattingProvider());
- languages.registerRenameProvider(editorLanguages, new RenameProvider());
-
- // Register our combined language and create dummy text models for some languages, to trigger their
- // initialization. Otherwise we will get errors when they are used by the combined language code.
- languages.register(msg);
-
- Monaco.createModel("", "typescript").dispose();
- Monaco.createModel("", "javascript").dispose();
- Monaco.createModel("", "json").dispose();
-
- if (languages.typescript) { // This field is not set when running under Jest.
- const compilerOptions: languages.typescript.CompilerOptions = {
- allowNonTsExtensions: true,
- target: languages.typescript.ScriptTarget.ESNext,
- module: languages.typescript.ModuleKind.ESNext,
- strictNullChecks: true,
- };
-
- languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions);
- languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions);
-
- // Disable the error about "top level await" for standalone editors.
- languages.typescript.typescriptDefaults.setDiagnosticsOptions({
- diagnosticCodesToIgnore: [1375],
- });
- languages.typescript.javascriptDefaults.setDiagnosticsOptions({
- diagnosticCodesToIgnore: [1375],
- });
- }
- }
-
/**
* A method that can be used to determine if the editor is currently scrolling its content because of mouse wheel
* events. This is used to correctly scroll embedded content like result panes in editor zones.
@@ -2324,14 +2207,4 @@ export class CodeEditor extends ComponentBase {
}
}
};
-
- static {
- CodeEditor.configureMonaco();
-
- requisitions.register("themeChanged", (data: IThemeChangeData): Promise => {
- CodeEditor.updateTheme(data.safeName, data.type, data.values);
-
- return Promise.resolve(true);
- });
- }
}
diff --git a/gui/frontend/src/components/ui/CodeEditor/CodeEditorSetup.ts b/gui/frontend/src/components/ui/CodeEditor/CodeEditorSetup.ts
new file mode 100644
index 000000000..685202e3e
--- /dev/null
+++ b/gui/frontend/src/components/ui/CodeEditor/CodeEditorSetup.ts
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version 2.0,
+ * as published by the Free Software Foundation.
+ *
+ * This program is designed to work with certain software (including
+ * but not limited to OpenSSL) that is licensed under separate terms, as
+ * designated in a particular file or component or in included license
+ * documentation. The authors of MySQL hereby grant you an additional
+ * permission to link the program and your derivative works with the
+ * separately licensed software that they have either included with
+ * the program or referenced in the documentation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
+ * the GNU General Public License, version 2.0, for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+import "./CodeEditor.css";
+
+import Color from "color";
+
+import "./userWorker.js";
+
+import { ILanguageDefinition, languages, Monaco } from "./index.js";
+
+import { msg } from "./languages/msg/msg.contribution.js";
+import { mysql } from "./languages/mysql/mysql.contribution.js";
+
+import { IThemeChangeData, IThemeObject, ITokenEntry } from "../../Theming/ThemeManager.js";
+
+import { requisitions } from "../../../supplement/Requisitions.js";
+import { CodeCompletionProvider } from "./CodeCompletionProvider.js";
+import { DefinitionProvider } from "./DefinitionProvider.js";
+import { DocumentHighlightProvider } from "./DocumentHighlightProvider.js";
+import { FormattingProvider } from "./FormattingProvider.js";
+import { HoverProvider } from "./HoverProvider.js";
+import { MsgSemanticTokensProvider } from "./MsgSemanticTokensProvider.js";
+import { ReferencesProvider } from "./ReferencesProvider.js";
+import { RenameProvider } from "./RenameProvider.js";
+import { SignatureHelpProvider } from "./SignatureHelpProvider.js";
+
+export class CodeEditorSetup {
+ private static monacoConfigured = false;
+
+ public static init(): void {
+ CodeEditorSetup.configureMonaco();
+
+ requisitions.register("themeChanged", (data: IThemeChangeData): Promise => {
+ CodeEditorSetup.updateTheme(data.safeName, data.type, data.values);
+
+ return Promise.resolve(true);
+ });
+ }
+
+ /**
+ * Called once to initialize various aspects of the monaco-editor subsystem (like languages, themes, options etc.)
+ */
+ private static configureMonaco(): void {
+ if (CodeEditorSetup.monacoConfigured) {
+ return;
+ }
+
+ CodeEditorSetup.monacoConfigured = true;
+
+ languages.onLanguage(msg.id, () => {
+ void msg.loader().then((definition: ILanguageDefinition) => {
+ languages.setMonarchTokensProvider(msg.id, definition.language);
+ languages.setLanguageConfiguration(msg.id, definition.languageConfiguration);
+ });
+ });
+
+ languages.onLanguage("mysql", () => {
+ void mysql.loader().then((definition: ILanguageDefinition) => {
+ languages.setMonarchTokensProvider("mysql", definition.language);
+ languages.setLanguageConfiguration("mysql", definition.languageConfiguration);
+ });
+ });
+
+ const editorLanguages = ["msg", "javascript", "typescript", "mysql", "python"];
+ languages.registerDocumentSemanticTokensProvider(editorLanguages, new MsgSemanticTokensProvider());
+ languages.registerCompletionItemProvider(editorLanguages, new CodeCompletionProvider());
+ languages.registerHoverProvider(editorLanguages, new HoverProvider());
+ languages.registerSignatureHelpProvider(editorLanguages, new SignatureHelpProvider());
+ languages.registerDocumentHighlightProvider(editorLanguages, new DocumentHighlightProvider());
+ languages.registerDefinitionProvider(editorLanguages, new DefinitionProvider());
+ languages.registerReferenceProvider(editorLanguages, new ReferencesProvider());
+ languages.registerDocumentFormattingEditProvider(editorLanguages, new FormattingProvider());
+ languages.registerRenameProvider(editorLanguages, new RenameProvider());
+
+ // Register our combined language and create dummy text models for some languages, to trigger their
+ // initialization. Otherwise we will get errors when they are used by the combined language code.
+ languages.register(msg);
+
+ Monaco.createModel("", "typescript").dispose();
+ Monaco.createModel("", "javascript").dispose();
+ Monaco.createModel("", "json").dispose();
+
+ if (languages.typescript) { // This field is not set when running under Jest.
+ const compilerOptions: languages.typescript.CompilerOptions = {
+ allowNonTsExtensions: true,
+ target: languages.typescript.ScriptTarget.ESNext,
+ module: languages.typescript.ModuleKind.ESNext,
+ strictNullChecks: true,
+ };
+
+ languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions);
+ languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions);
+
+ // Disable the error about "top level await" for standalone editors.
+ languages.typescript.typescriptDefaults.setDiagnosticsOptions({
+ diagnosticCodesToIgnore: [1375],
+ });
+ languages.typescript.javascriptDefaults.setDiagnosticsOptions({
+ diagnosticCodesToIgnore: [1375],
+ });
+ }
+ }
+
+ /**
+ * Updates the theme used by all code editor instances.
+ *
+ * @param theme The theme name (DOM safe).
+ * @param type The base type of the theme.
+ * @param values The actual theme values.
+ */
+ private static updateTheme(theme: string, type: "light" | "dark", values: IThemeObject): void {
+ Monaco.remeasureFonts();
+
+ // Convert all color values to CSS hex form.
+ const entries: { [key: string]: string; } = {};
+ for (const [key, value] of Object.entries(values.colors || {})) {
+ entries[key] = (new Color(value)).hexa();
+ }
+
+ const tokenRules: Monaco.ITokenThemeRule[] = [];
+ (values.tokenColors || []).forEach((value: ITokenEntry): void => {
+ const scopeValue = value.scope || [];
+ const scopes = Array.isArray(scopeValue) ? scopeValue : scopeValue.split(",");
+ scopes.forEach((scope: string): void => {
+ tokenRules.push({
+ token: scope,
+ foreground: (new Color(value.settings.foreground)).hexa(),
+ background: (new Color(value.settings.background)).hexa(),
+ fontStyle: value.settings.fontStyle,
+ });
+ });
+ });
+
+ Monaco.defineTheme(theme, {
+ base: type === "light" ? "vs" : "vs-dark",
+ inherit: false,
+ rules: tokenRules,
+ colors: entries,
+ });
+
+ Monaco.setTheme(theme);
+ }
+}
diff --git a/gui/frontend/src/components/ui/Codicon.ts b/gui/frontend/src/components/ui/Codicon.ts
index fc3887cc2..c6879a32b 100644
--- a/gui/frontend/src/components/ui/Codicon.ts
+++ b/gui/frontend/src/components/ui/Codicon.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
@@ -430,6 +430,7 @@ export enum Codicon {
TriangleRight,
TriangleUp,
Twitter,
+ TypeHierarchySub,
Unfold,
UngroupByRefType,
Unlock,
@@ -862,6 +863,7 @@ export const iconNameMap = new Map([
[Codicon.TriangleRight, "triangle-right"],
[Codicon.TriangleUp, "triangle-up"],
[Codicon.Twitter, "twitter"],
+ [Codicon.TypeHierarchySub, "type-hierarchy-sub"],
[Codicon.Unfold, "unfold"],
[Codicon.UngroupByRefType, "ungroup-by-ref-type"],
[Codicon.Unlock, "unlock"],
diff --git a/gui/frontend/src/components/ui/ConnectionTile/ConnectionTile.css b/gui/frontend/src/components/ui/ConnectionTile/ConnectionTile.css
new file mode 100644
index 000000000..2c3d5b3b3
--- /dev/null
+++ b/gui/frontend/src/components/ui/ConnectionTile/ConnectionTile.css
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version 2.0,
+ * as published by the Free Software Foundation.
+ *
+ * This program is designed to work with certain software (including
+ * but not limited to OpenSSL) that is licensed under separate terms, as
+ * designated in a particular file or component or in included license
+ * documentation. The authors of MySQL hereby grant you an additional
+ * permission to link the program and your derivative works with the
+ * separately licensed software that they have either included with
+ * the program or referenced in the documentation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
+ * the GNU General Public License, version 2.0, for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+.msg.connectionTile {
+ display: flex;
+ flex-direction: row;
+ position: relative;
+
+ align-items: stretch;
+
+ margin: 2px;
+ padding: 0 0 0 20px;
+ width: 270px;
+ height: 80px;
+
+ border-radius: 4px;
+ border: 2px solid var(--connectionTile-border);
+
+ color: var(--connectionTile-foreground);
+ background-color: var(--connectionTile-background);
+
+ transition: background .15s ease-in;
+ user-select: none;
+}
+
+/* Need a stronger focus indicator for this big buttons. A single thin line doesn't work well. */
+.msg.connectionTile:focus,
+.msg.connectionTile:focus-within {
+ outline: none;
+ border-color: var(--connectionTileCreateNew-activeBackground);
+ color: var(--connectionTile-activeForeground);
+ background-color: var(--connectionTile-activeBackground);
+}
+
+.msg.connectionTile:focus::after {
+ content: '';
+ display: block;
+ position: absolute;
+ top: -4px;
+ bottom: -4px;
+ left: -4px;
+ right: -4px;
+ border-radius: 4px;
+ border: 2px dashed var(--focusBorder);
+ overflow: visible;
+}
+
+.msg.button.connectionTile .icon {
+ flex: 0 0 40px;
+ margin-right: 16px;
+
+ /* for codicons */
+ font-size: 28pt;
+ align-content: center;
+
+ color: var(--connectionTile-foreground);
+}
+
+.msg.connectionTile:not(:disabled).dropTarget,
+.msg.connectionTile:not(:disabled):hover {
+ background-color: var(--connectionTile-hoverBackground);
+}
+
+.msg.connectionTile:not(:disabled):active {
+ color: var(--connectionTile-activeForeground);
+ border-color: var(--connectionTile-activeBorder);
+ background-color: var(--connectionTile-activeBackground);
+}
+
+.msg.connectionTile.createNew:not(:disabled).dropTarget,
+.msg.connectionTile.createNew:not(:disabled):hover {
+ background-color: var(--connectionTileCreateNew-hoverBackground);
+}
+
+.msg.connectionTile.createNew:not(:disabled):active {
+ color: var(--connectionTileCreateNew-activeForeground);
+ border-color: var(--connectionTileCreateNew-activeBorder);
+ background-color: var(--connectionTileCreateNew-activeBackground);
+}
+
+.msg.connectionTile.createNew {
+ border-color: var(--connectionTileCreateNew-border);
+ background-color: var(--connectionTileCreateNew-background);
+ color: var(--connectionTileCreateNew-foreground);
+}
+
+.msg.connectionTile.createNew:focus {
+ color: var(--connectionTileCreateNew-activeForeground);
+ border-color: transparent;
+ background-color: var(--connectionTileCreateNew-activeBackground);
+}
+
+.msg.connectionTile.group {
+ border-color: var(--connectionTileGroup-border);
+ background-color: var(--connectionTileGroup-background);
+ color: var(--connectionTileGroup-foreground);
+}
+
+.msg.connectionTile.group:not(:disabled).dropTarget,
+.msg.connectionTile.group:not(:disabled):hover {
+ background-color: var(--connectionTileGroup-hoverBackground);
+}
+
+.msg.connectionTile.group:not(:disabled):active {
+ color: var(--connectionTileGroup-activeForeground);
+ border-color: var(--connectionTileGroup-activeBorder);
+ background-color: var(--connectionTileGroup-activeBackground);
+}
+
+.msg.connectionTile.group:focus {
+ color: var(--connectionTileGroup-activeForeground);
+ border-color: transparent;
+ background-color: var(--connectionTileGroup-activeBackground);
+}
+
+.msg.connectionTile.back {
+ border-color: var(--connectionTileBack-border);
+ background-color: var(--connectionTileBack-background);
+ color: var(--connectionTileBack-foreground);
+}
+
+.msg.connectionTile.back:not(:disabled).dropTarget,
+.msg.connectionTile.back:not(:disabled):hover {
+ background-color: var(--connectionTileBack-hoverBackground);
+}
+
+.msg.connectionTile.back:not(:disabled):active {
+ color: var(--connectionTileBack-activeForeground);
+ border-color: var(--connectionTileBack-activeBorder);
+ background-color: var(--connectionTileBack-activeBackground);
+}
+
+.msg.connectionTile.back:focus {
+ color: var(--connectionTileBack-activeForeground);
+ border-color: transparent;
+ background-color: var(--connectionTileBack-activeBackground);
+}
+
+.msg.connectionTile .textHost {
+ flex: 1 1 auto;
+ justify-content: center;
+ padding-right: 39px;
+}
+
+.msg.connectionTile .label {
+ margin: 0;
+ padding: 0;
+
+ text-align: left;
+
+ color: var(--connectionTile-foreground);
+}
+
+.msg.connectionTile .tileCaption {
+ font-size: 1.15rem;
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.msg.connectionTile .tileDescription {
+ font-size: 0.8rem;
+}
+
+.msg.connectionTile.secondary .label {
+ color: var(--connectionTileCreateNew--foreground);
+}
+
+.msg.connectionTile.secondary .icon {
+ background-color: var(--connectionTileCreateNew-foreground);
+}
+
+.msg.connectionTile #actionsBackground {
+ visibility: hidden;
+ position: absolute;
+
+ top: 0;
+ right: 0;
+ margin: 0;
+
+ width: 39px;
+ height: 76px;
+
+ border: none;
+ background: var(--connectionTile-border);
+ opacity: 35%;
+}
+
+.msg.connectionTile #actions {
+ display: flex;
+ visibility: hidden;
+ position: absolute;
+ justify-items: center;
+
+ top: 0;
+ right: 0;
+ margin: 0;
+
+ width: 39px;
+ height: 76px;
+
+ border: none;
+ background: none;
+}
+
+.msg.connectionTile:focus #actions,
+.msg.connectionTile:hover #actions,
+.msg.connectionTile:focus #actionsBackground,
+.msg.connectionTile:hover #actionsBackground {
+ visibility: visible;
+}
+
+.msg.connectionTile #actions > #triggerEditMenu {
+ flex: 1 1 auto;
+
+ min-width: 0;
+ width: 17px;
+ height: 17px;
+
+ padding: 0;
+ margin: 0;
+
+ color: var(--icon-foreground);
+ font-weight: 800;
+
+ text-align: center;
+ border: none;
+ background: var(--connectionTile-border);
+}
+
+.msg.connectionTile #actions .button {
+ margin: 0;
+ padding: 0;
+ min-width: 0;
+ background: none;
+ border: none;
+ color: var(--connectionTile-foreground);
+ width: 24px;
+ height: 24px;
+
+ opacity: 100%;
+}
+
+.msg.connectionTile #actions .icon {
+ flex: 0 0 16px;
+ height: 16px;
+ margin: 0;
+}
diff --git a/gui/frontend/src/components/ui/ConnectionTile/ConnectionTile.tsx b/gui/frontend/src/components/ui/ConnectionTile/ConnectionTile.tsx
index 5e8584bdc..72b68f1e8 100644
--- a/gui/frontend/src/components/ui/ConnectionTile/ConnectionTile.tsx
+++ b/gui/frontend/src/components/ui/ConnectionTile/ConnectionTile.tsx
@@ -23,32 +23,108 @@
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
-import { ComponentChild } from "preact";
+import "./ConnectionTile.css";
-import type { ICdmConnectionEntry } from "../../../data-models/ConnectionDataModel.js";
+import { ComponentChild, createRef } from "preact";
+
+import type { ICdmConnectionEntry, ICdmConnectionGroupEntry } from "../../../data-models/ConnectionDataModel.js";
import { Assets } from "../../../supplement/Assets.js";
-import { BrowserTile, IBrowserTileProperties } from "../BrowserTile/BrowserTile.js";
+import { KeyboardKeys } from "../../../utilities/helpers.js";
+import { filterInt } from "../../../utilities/string-helpers.js";
import { Button } from "../Button/Button.js";
+import type { Codicon } from "../Codicon.js";
+import { ComponentBase, DragEventType, type IComponentProperties } from "../Component/ComponentBase.js";
+import { Container, ContentAlignment, Orientation } from "../Container/Container.js";
import { Icon } from "../Icon/Icon.js";
+import { Label } from "../Label/Label.js";
+
+/** The tile type determines the presentation of the tile. */
+export enum ConnectionTileType {
+ /** Activates the tile. */
+ Open = 0,
+
+ /** Triggers creation of a new entry. */
+ CreateNew = 1,
+
+ /** Identifies this tile as being a group. */
+ Group = 2,
-export interface IConnectionTileProperties extends IBrowserTileProperties {
- entry?: ICdmConnectionEntry;
+ /** The tile to return from a group to the next upper level. */
+ Back = 3,
}
-export class ConnectionTile extends BrowserTile {
+export interface ITileActionOptions {
+ newTab?: boolean;
+ target?: HTMLElement | null;
+ editor?: "notebook" | "script";
+}
- public static override defaultProps = {
- className: "connectionTile",
- };
+export interface IConnectionTileProperties extends IComponentProperties {
+ entry?: ICdmConnectionEntry | ICdmConnectionGroupEntry;
+
+ tileId: string;
+ caption: string;
+ description: string;
+ type: ConnectionTileType;
+ icon: string | Codicon;
+
+ onAction?: (action: string, props: IConnectionTileProperties, options: ITileActionOptions) => void;
+ onTileReorder?: (draggedTileId: number, props: unknown) => void;
+}
+
+export class ConnectionTile extends ComponentBase {
+
+ private actionsRef = createRef();
public constructor(props: IConnectionTileProperties) {
super(props);
- this.addHandledProperties("details");
+ this.addHandledProperties("tileId", "caption", "description", "type", "icon", "onAction", "onTileReorder");
+ this.connectDragEvents();
}
- public override render(): ComponentChild {
- return super.render();
+ public render(): ComponentChild {
+ const { tileId, type, icon, caption, description } = this.props;
+
+ const className = this.getEffectiveClassNames([
+ "connectionTile",
+ this.classFromProperty(type, ["", "createNew", "group", "back"]),
+ ]);
+
+ const actions = this.renderTileActionUI();
+
+ return (
+
+ );
}
protected renderTileActionUI = (): ComponentChild => {
@@ -82,6 +158,61 @@ export class ConnectionTile extends BrowserTile {
);
};
+ protected override handleDragEvent = (type: DragEventType, e: DragEvent): boolean => {
+ if (!e.dataTransfer) {
+ return false;
+ }
+
+ const element = e.currentTarget as HTMLElement;
+ switch (type) {
+ case DragEventType.Start: {
+ e.dataTransfer.effectAllowed = "move";
+ e.dataTransfer.setData("browser/tile", element.id);
+
+ return true;
+ }
+
+ case DragEventType.Enter: {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (element.contains(e.relatedTarget as Node)) {
+ element.classList.add("dropTarget");
+ }
+
+ return true;
+ }
+
+ case DragEventType.Leave: {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (!element.contains(e.relatedTarget as Node)) {
+ element.classList.remove("dropTarget");
+ }
+
+ return true;
+ }
+
+ case DragEventType.Drop: {
+ element.classList.remove("dropTarget");
+
+ const id = filterInt(e.dataTransfer.getData("browser/tile"));
+ if (!isNaN(id)) {
+ const { onTileReorder } = this.props;
+ onTileReorder?.(id, this.props);
+ }
+
+ return true;
+ }
+
+ default: {
+ return false;
+ }
+ }
+
+ };
+
private handleActionClick = (e: MouseEvent | KeyboardEvent): void => {
const { onAction } = this.props;
@@ -110,4 +241,36 @@ export class ConnectionTile extends BrowserTile {
}
};
+ private handleClick = (e: MouseEvent | KeyboardEvent): void => {
+ const { onAction, type } = this.props;
+
+ const event = e as MouseEvent;
+ const button = event.currentTarget as HTMLButtonElement;
+
+ // Have to prevent double clicks on browser tiles. But since everything is async there's no way to know
+ // the action triggered by the tile is finished (or at least started, so the button is hidden).
+ // Hence the only way to enable the button is to use a timer.
+ button.disabled = true;
+ setTimeout(() => {
+ button.disabled = false;
+ }, 200);
+ e.stopPropagation();
+
+ if (type === ConnectionTileType.Open) {
+ onAction?.("open", this.props, { newTab: event.metaKey || event.altKey });
+ } else {
+ onAction?.(this.props.tileId === "-1" ? "new" : "", this.props, {});
+ }
+ };
+
+ private handleKeydown = (e: KeyboardEvent): void => {
+ if (e.key === KeyboardKeys.A) { // Unmodified A key.
+ e.stopPropagation();
+ const { type, onAction } = this.props;
+ if (type === ConnectionTileType.Open) {
+ this.actionsRef.current?.focus();
+ onAction?.("menu", this.props, { target: this.actionsRef.current });
+ }
+ }
+ };
}
diff --git a/gui/frontend/src/components/ui/Container/Container.css b/gui/frontend/src/components/ui/Container/Container.css
index 223ce76ac..d8d4edc50 100644
--- a/gui/frontend/src/components/ui/Container/Container.css
+++ b/gui/frontend/src/components/ui/Container/Container.css
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
@@ -26,7 +26,6 @@
.msg.container {
display: flex;
position: relative;
- overflow: auto;
}
.msg.container.topLeft {
diff --git a/gui/frontend/src/components/ui/FileSelector/FileSelector.tsx b/gui/frontend/src/components/ui/FileSelector/FileSelector.tsx
index 26ebf216c..b04a403f5 100644
--- a/gui/frontend/src/components/ui/FileSelector/FileSelector.tsx
+++ b/gui/frontend/src/components/ui/FileSelector/FileSelector.tsx
@@ -27,10 +27,10 @@ import "./FileSelector.css";
import { ComponentChild } from "preact";
+import { appParameters } from "../../../supplement/AppParameters.js";
import { IOpenDialogFilters, IOpenFileDialogResult } from "../../../supplement/RequisitionTypes.js";
import { requisitions } from "../../../supplement/Requisitions.js";
-import { appParameters } from "../../../supplement/AppParameters.js";
-import { selectFile } from "../../../utilities/helpers.js";
+import { selectFileInBrowser } from "../../../utilities/helpers.js";
import { Button } from "../Button/Button.js";
import { ComponentBase, IComponentProperties } from "../Component/ComponentBase.js";
import { Container, Orientation } from "../Container/Container.js";
@@ -190,7 +190,7 @@ export class FileSelector extends ComponentBase {
});
}
- void selectFile(contentType, multiSelection).then((result) => {
+ void selectFileInBrowser(contentType, multiSelection).then((result) => {
if (result) {
onChange?.(result.map((value) => { return value.name; }), this.props);
} else {
diff --git a/gui/frontend/src/components/ui/FrontPage/FrontPage.css b/gui/frontend/src/components/ui/FrontPage/FrontPage.css
index ffd83e447..da350c108 100644
--- a/gui/frontend/src/components/ui/FrontPage/FrontPage.css
+++ b/gui/frontend/src/components/ui/FrontPage/FrontPage.css
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
@@ -35,7 +35,6 @@
.msg.frontPage #title {
font-size: 2.2rem;
- padding: 0 !important;
font-weight: 200;
margin-top: 30px;
margin-bottom: 20px;
@@ -77,7 +76,6 @@
}
.msg.frontPage #contentTitle {
- padding-top: 10px;
font-size: 110%;
}
diff --git a/gui/frontend/src/components/ui/Icon/Icon.css b/gui/frontend/src/components/ui/Icon/Icon.css
index 2708f5b8d..07b7a4228 100644
--- a/gui/frontend/src/components/ui/Icon/Icon.css
+++ b/gui/frontend/src/components/ui/Icon/Icon.css
@@ -26,6 +26,11 @@
.msg .iconHost {
flex-shrink: 0;
position: relative;
+ width: 16px;
+}
+
+.msg .iconHost.data {
+ width: auto;
}
.msg.icon:not(.codicon) {
@@ -34,8 +39,6 @@
}
.msg.icon.withOverlay {
- width: 16px;
-
mask-size: 100% 100%;
mask-repeat: no-repeat;
mask-composite: subtract;
diff --git a/gui/frontend/src/components/ui/Icon/Icon.tsx b/gui/frontend/src/components/ui/Icon/Icon.tsx
index b65d25570..6864f3090 100644
--- a/gui/frontend/src/components/ui/Icon/Icon.tsx
+++ b/gui/frontend/src/components/ui/Icon/Icon.tsx
@@ -78,7 +78,7 @@ export class Icon extends ComponentBase {
let maskSize: string | undefined = "100% 100%";
let maskComposite: string | undefined = "subtract";
const olLayers: ComponentChild[] = [];
- if (overlays) {
+ if (overlays && overlays.length > 0) {
className += " withOverlay";
overlays.forEach((overlay) => {
diff --git a/gui/frontend/src/components/ui/Label/Label.css b/gui/frontend/src/components/ui/Label/Label.css
index 769030fe1..39d1d70fb 100644
--- a/gui/frontend/src/components/ui/Label/Label.css
+++ b/gui/frontend/src/components/ui/Label/Label.css
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
@@ -112,6 +112,12 @@
color: var(--terminal-ansiGreen);
}
+.msg.resultText.log,
+.msg.label.log {
+ padding: 0 4px;
+ color: var(--terminal-ansiBrightBlue);
+}
+
.msg.resultText.interactive,
.msg.resultText.interactive {
background-color: hsla(224, 58%, 50%, 0.1);
@@ -126,4 +132,4 @@
50% {
opacity: 0;
}
-}
+}
\ No newline at end of file
diff --git a/gui/frontend/src/components/ui/Label/Label.tsx b/gui/frontend/src/components/ui/Label/Label.tsx
index 50404326e..e14ef5cb0 100644
--- a/gui/frontend/src/components/ui/Label/Label.tsx
+++ b/gui/frontend/src/components/ui/Label/Label.tsx
@@ -118,7 +118,7 @@ export class Label extends ComponentBase {
if (language === "ansi" && labelEntries) {
const className = this.getEffectiveClassNames([
"resultText",
- this.classFromProperty(type, ["error", "warning", "info", "text", "response", "success"]),
+ this.classFromProperty(type, ["error", "warning", "info", "text", "response", "success", "log"]),
this.classFromProperty(quoted, "quote"),
this.classFromProperty(code, "code"),
this.classFromProperty(heading, "heading"),
diff --git a/gui/frontend/src/components/ui/Portal/Portal.tsx b/gui/frontend/src/components/ui/Portal/Portal.tsx
index 42c37ec03..5995ea6cc 100644
--- a/gui/frontend/src/components/ui/Portal/Portal.tsx
+++ b/gui/frontend/src/components/ui/Portal/Portal.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
@@ -104,6 +104,7 @@ export class Portal extends ComponentBase {
if (id) {
this.host.id = id;
}
+
this.host.className = className;
this.host.style.setProperty("--background-opacity", String(options.backgroundOpacity ?? 0.5));
this.host.addEventListener("mousedown", this.handlePortalMouseDown);
diff --git a/gui/frontend/src/components/ui/Tabview/Tabview.tsx b/gui/frontend/src/components/ui/Tabview/Tabview.tsx
index 45f595b4a..5186a3867 100644
--- a/gui/frontend/src/components/ui/Tabview/Tabview.tsx
+++ b/gui/frontend/src/components/ui/Tabview/Tabview.tsx
@@ -201,7 +201,7 @@ export class Tabview extends ComponentBase {
public render(): ComponentChild {
const {
tabPosition, stretchTabs, hideSingleTab, pages, tabBorderWidth, style, contentSeparatorWidth, selectedId,
- showTabs, canReorderTabs, auxillary,
+ showTabs, canReorderTabs, auxillary, closeTabs,
} = this.props;
const className = this.getEffectiveClassNames([
@@ -322,7 +322,7 @@ export class Tabview extends ComponentBase {
{auxillary && {auxillary}}
- {this.props.closeTabs && (
+ {closeTabs && (
-
+
+
+
+
-
+
);
}
@@ -252,11 +334,13 @@ export class ConnectionBrowser extends ComponentBase => {
- const connections = this.connections;
- const details = connections.find((candidate) => { return candidate.details.id === connectionId; });
+ const connections = this.dataModel?.connectionList;
+ const connection = connections?.find((candidate) => {
+ return candidate.type === CdmEntityType.Connection && candidate.details.id === connectionId;
+ });
- if (details) {
- this.doHandleTileAction("edit", details, undefined);
+ if (connection) {
+ this.doHandleTileAction("edit", connection, undefined);
return Promise.resolve(true);
}
@@ -265,11 +349,14 @@ export class ConnectionBrowser extends ComponentBase => {
- const connections = this.connections;
+ const connections = this.dataModel?.connectionList;
- const details = connections.find((candidate) => { return candidate.details.id === connectionId; });
- if (details) {
- this.doHandleTileAction("remove", details, undefined);
+ const connection = connections?.find((candidate) => {
+ return candidate.type === CdmEntityType.Connection && candidate.details.id === connectionId;
+ });
+
+ if (connection) {
+ this.doHandleTileAction("remove", connection, undefined);
return Promise.resolve(true);
}
@@ -278,11 +365,14 @@ export class ConnectionBrowser extends ComponentBase => {
- const connections = this.connections;
+ const connections = this.dataModel?.connectionList;
+
+ const connection = connections?.find((candidate) => {
+ return candidate.type === CdmEntityType.Connection && candidate.details.id === connectionId;
+ });
- const details = connections.find((candidate) => { return candidate.details.id === connectionId; });
- if (details) {
- this.doHandleTileAction("duplicate", details, undefined);
+ if (connection) {
+ this.doHandleTileAction("duplicate", connection, undefined);
return Promise.resolve(true);
}
@@ -291,7 +381,8 @@ export class ConnectionBrowser extends ComponentBase => {
- if (entry?.key === "dbEditor.connectionBrowser.showGreeting") {
+ if (entry?.key === "dbEditor.connectionBrowser.showGreeting"
+ || entry?.key === "dbEditor.connectionBrowser.sortFoldersFirst") {
this.forceUpdate();
return Promise.resolve(true);
@@ -300,6 +391,31 @@ export class ConnectionBrowser extends ComponentBase => {
+ if (groupId !== undefined && groupId >= 0) {
+ const context = this.context as DocumentContextType;
+ if (context) {
+ const group = context.connectionsDataModel.findConnectionGroupEntryById(groupId);
+ if (group) {
+ await group.refresh?.();
+ this.forceUpdate();
+ } else {
+ await context.connectionsDataModel.reloadConnections();
+ }
+ }
+ }
+
+ return Promise.resolve(true);
+ };
+
private handleCloseGreeting = (): void => {
Settings.set("dbEditor.connectionBrowser.showGreeting", false);
};
@@ -310,12 +426,17 @@ export class ConnectionBrowser extends ComponentBase {
const tileProps = props as IConnectionTileProperties;
- this.doHandleTileAction(action, tileProps?.entry, options as IDictionary);
+ if (action === "new") {
+ this.doHandleTileAction(action, undefined, options as IDictionary);
+ } else {
+ this.doHandleTileAction(action, tileProps?.entry, options as IDictionary);
+ }
};
- private doHandleTileAction = (action: string, entry: ICdmConnectionEntry | undefined,
+ private doHandleTileAction = (action: string, entry: ICdmConnectionEntry | ICdmConnectionGroupEntry | undefined,
options?: ITileActionOptions): void => {
- const dbType = entry?.details.dbType ?? DBType.MySQL;
+
+ let dbType: DBType | undefined;
+
+ if (entry?.type === CdmEntityType.Connection) {
+ dbType = entry?.details ? entry.details.dbType : DBType.MySQL;
+ } else if (action === "new") {
+ void this.editorRef.current?.show(DBType.MySQL, true, this.generateFolderPathFromCurrentGroup());
+
+ return;
+ } else {
+ // Can only be group activation. If entry is undefined, we have to go up one level.
+ if (!entry) {
+ let { currentGroup } = this.state;
+ if (currentGroup && currentGroup.parent) { // Just a sanity check. You cannot go up on root level.
+ currentGroup = currentGroup.parent;
+ void currentGroup.refresh?.().then(() => {
+ if (currentGroup!.folderPath.caption === "/") {
+ // This is the root group - no parent.
+ currentGroup = undefined;
+ }
+
+ this.setState({ currentGroup });
+ });
+ }
+ } else {
+ void entry.refresh?.().then(() => {
+ this.setState({ currentGroup: entry });
+ });
+ }
+
+ return;
+ }
switch (action) {
case "menu": {
@@ -406,48 +564,12 @@ export class ConnectionBrowser extends ComponentBase 0) {
- host = options.endpoints[0].ipAddress;
- }
-
- const connectionDetails: IConnectionDetails = {
- id: -1,
- caption: options.displayName as string ?? "",
- dbType: DBType.MySQL,
- description: options.description as string ?? "",
- useMHS: true,
- useSSH: false,
- options: {
- /* eslint-disable @typescript-eslint/naming-convention */
- "compartment-id": options.compartmentId,
- "mysql-db-system-id": options.id,
- "profile-name": options.profileName ?? "DEFAULT",
-
- // Disable support for bastion to be stored in freeform tags for the time being
- // "bastion-id": (options.freeformTags as IDictionary)?.bastionId ?? undefined,
- host,
- "scheme": MySQLConnectionScheme.MySQL,
- /* eslint-enable @typescript-eslint/naming-convention */
- },
- };
- void this.editorRef.current?.show(dbType, true, connectionDetails);
-
- } else {
- void this.editorRef.current?.show(dbType, true);
- }
-
+ void this.editorRef.current?.show(dbType, false, "", entry.details);
break;
}
case "duplicate": {
- void this.editorRef.current?.show(dbType, true, entry?.details);
+ void this.editorRef.current?.show(dbType, true, "", entry.details);
break;
}
@@ -564,7 +686,7 @@ export class ConnectionBrowser extends ComponentBase {
+ return (this.context as DocumentContextType)?.connectionsDataModel.roots ?? [];
}
private get dataModel(): ConnectionDataModel | undefined {
return (this.context as DocumentContextType)?.connectionsDataModel;
}
- private dataModelChanged = (): void => {
+ private dataModelChanged = (actions: Readonly): void => {
+ if (actions.length === 1) {
+ // Check if our current group was removed.
+ const { currentGroup } = this.state;
+
+ const action = actions[0];
+ if (action.action === "remove" && action.entry === currentGroup) {
+ if (currentGroup?.parent) {
+ this.setState({
+ currentGroup: currentGroup.parent.folderPath.id === 1 ? undefined : currentGroup.parent,
+ });
+ }
+ }
+ }
+
this.forceUpdate();
};
+
+ private handleSelectPath = (path: number[]): void => {
+ const dataModel = this.dataModel;
+ if (dataModel) {
+ const group = dataModel.findConnectionGroupById(path);
+ if (group) {
+ // Set undefined for the root group.
+ this.setState({ currentGroup: group.folderPath.id === 1 ? undefined : group });
+ }
+ }
+ };
+
+ private generateFolderPathFromCurrentGroup(): string {
+ const { currentGroup } = this.state;
+
+ const path: string[] = [];
+ let runner = currentGroup;
+ while (runner) {
+ path.unshift(runner.caption);
+ runner = runner.parent;
+ }
+
+ return path.join("/");
+ }
}
diff --git a/gui/frontend/src/modules/db-editor/ConnectionDataModelListener.ts b/gui/frontend/src/modules/db-editor/ConnectionDataModelListener.ts
index f563f52a8..d05db6146 100644
--- a/gui/frontend/src/modules/db-editor/ConnectionDataModelListener.ts
+++ b/gui/frontend/src/modules/db-editor/ConnectionDataModelListener.ts
@@ -37,9 +37,9 @@ export class ConnectionDataModelListener {
*
* @returns A promise always fulfilled to true.
*/
- public handleConnectionAdded = (details: IConnectionDetails): Promise => {
+ public handleConnectionAdded = async (details: IConnectionDetails): Promise => {
const entry = this.connectionsDataModel.createConnectionEntry(details);
- this.connectionsDataModel.addConnectionEntry(entry);
+ await this.connectionsDataModel.addConnectionEntry(entry);
return Promise.resolve(true);
};
@@ -51,7 +51,7 @@ export class ConnectionDataModelListener {
*
* @returns A promise always fulfilled to true.
*/
- public handleConnectionUpdated = (details: IConnectionDetails): Promise => {
+ public handleConnectionUpdated = async (details: IConnectionDetails): Promise => {
this.connectionsDataModel.updateConnectionDetails(details);
return Promise.resolve(true);
diff --git a/gui/frontend/src/modules/db-editor/ConnectionEditor.tsx b/gui/frontend/src/modules/db-editor/ConnectionEditor.tsx
index 0527d2bca..1345b481f 100644
--- a/gui/frontend/src/modules/db-editor/ConnectionEditor.tsx
+++ b/gui/frontend/src/modules/db-editor/ConnectionEditor.tsx
@@ -24,8 +24,10 @@
*/
import { ComponentChild, createRef } from "preact";
-import { DialogResponseClosure, IDictionary, IServicePasswordRequest } from "../../app-logic/general-types.js";
+import {
+ DialogResponseClosure, DialogType, IDictionary, IServicePasswordRequest,
+} from "../../app-logic/general-types.js";
import {
IMySQLConnectionOptions, MySQLConnCompression, MySQLConnectionScheme, MySQLSqlMode, MySQLSslMode,
} from "../../communication/MySQL.js";
@@ -38,6 +40,7 @@ import {
} from "../../components/Dialogs/ValueEditDialog.js";
import { CheckState, ICheckboxProperties } from "../../components/ui/Checkbox/Checkbox.js";
+import { DialogHost } from "../../app-logic/DialogHost.js";
import { ui } from "../../app-logic/UILayer.js";
import { ComponentBase, IComponentProperties, IComponentState } from "../../components/ui/Component/ComponentBase.js";
import { Container, ContentAlignment, ContentWrap, Orientation } from "../../components/ui/Container/Container.js";
@@ -45,12 +48,12 @@ import { Grid } from "../../components/ui/Grid/Grid.js";
import { GridCell } from "../../components/ui/Grid/GridCell.js";
import { Label } from "../../components/ui/Label/Label.js";
import { ProgressIndicator } from "../../components/ui/ProgressIndicator/ProgressIndicator.js";
-import type { ICdmConnectionEntry } from "../../data-models/ConnectionDataModel.js";
+import { CdmEntityType } from "../../data-models/ConnectionDataModel.js";
import { requisitions } from "../../supplement/Requisitions.js";
import { Settings } from "../../supplement/Settings/Settings.js";
import { ShellInterface } from "../../supplement/ShellInterface/ShellInterface.js";
import { ShellInterfaceShellSession } from "../../supplement/ShellInterface/ShellInterfaceShellSession.js";
-import { DBConnectionEditorType, DBType, IConnectionDetails } from "../../supplement/ShellInterface/index.js";
+import { ConnectionEditorType, DBType, IConnectionDetails } from "../../supplement/ShellInterface/index.js";
import { RunMode, webSession } from "../../supplement/WebSession.js";
import { convertErrorToString } from "../../utilities/helpers.js";
import { basename, filterInt } from "../../utilities/string-helpers.js";
@@ -89,6 +92,9 @@ export class ConnectionEditor extends ComponentBase {
+ public async show(dbTypeName: string, newConnection: boolean, currentPath: string,
+ details?: IConnectionDetails): Promise {
if (this.knowDbTypes.length === 0) {
this.knowDbTypes = await ShellInterface.core.getDbTypes();
}
+ await this.loadKnownFolders();
this.liveUpdateFields.bastionName.value = "";
this.liveUpdateFields.mdsDatabaseName.value = "";
@@ -203,7 +212,7 @@ export class ConnectionEditor extends ComponentBase {
- return element.details.caption === informationSection.caption.value;
+ const roots = context.connectionsDataModel.roots;
+ const entry = roots.find((element) => {
+ if (element.type === CdmEntityType.Connection) {
+ return element.details.caption === informationSection.caption.value;
+ }
+
+ return undefined;
});
// The caption can be the same if it is the one from the connection that is currently being edited.
- if (entry && (String(entry.details.id) !== values.id || data?.createNew)) {
+ if (entry?.type === CdmEntityType.Connection
+ && (String(entry.details.id) !== values.id || data?.createNew)) {
result.messages.caption = "A connection with that caption exists already";
}
}
@@ -380,6 +394,8 @@ export class ConnectionEditor extends ComponentBase {
+ private generateEditorConfig = (currentPath: string, details?: IConnectionDetails): IDialogValues => {
const context = this.context as DocumentContextType;
- const connections = context.connectionsDataModel.connections;
+ const connections = context.connectionsDataModel.roots;
let caption = `New Connection`;
let index = 1;
while (index < 1000) {
const candidate = `New Connection ${index}`;
- if (connections.findIndex((element: ICdmConnectionEntry) => {
- return element.details.caption === candidate;
+ if (connections.findIndex((element) => {
+ return element.type === CdmEntityType.Connection && element.details.caption === candidate;
}) === -1) {
caption = candidate;
@@ -525,12 +540,12 @@ export class ConnectionEditor extends ComponentBase {
+ private loadMdsAdditionalDataAndShowConnectionDlg(dbTypeName: string, newConnection: boolean,
+ currentPath: string, details?: IConnectionDetails): void {
const contexts: string[] = [dbTypeName];
if (details?.useMHS) {
contexts.push("useMDS");
@@ -1124,7 +1143,7 @@ export class ConnectionEditor extends ComponentBase {
this.updateInputValue("", "mysqlDbSystemName");
});
- });
+ }
private handleBastionIdChange = (value: string, _dialog: ValueEditDialog, forceUpdate = false): void => {
if (value !== this.liveUpdateFields.bastionId.value || forceUpdate) {
@@ -1272,7 +1291,7 @@ export class ConnectionEditor extends ComponentBase {
+ private confirmBastionCreation = (currentPath: string, connection?: IConnectionDetails): void => {
if (this.confirmNewBastionDialogRef.current) {
this.confirmNewBastionDialogRef.current.show(
(
@@ -1291,15 +1310,16 @@ export class ConnectionEditor extends ComponentBase {
+ private handleCreateNewBastion = ((closure: DialogResponseClosure, values?: IDictionary): void => {
if (closure === DialogResponseClosure.Accept && values) {
const details = values.connection as IConnectionDetails;
+ const currentPath = values.currentPath as string ?? "/";
const contexts: string[] = [DBType.MySQL];
if (details.useSSH) {
contexts.push("useSSH");
@@ -1313,7 +1333,7 @@ export class ConnectionEditor extends ComponentBase {
+ const values = dialog.getDialogValues();
+ const generalSection = values.sections.get("general")!.values;
+ const folderPath = generalSection.folderPath.value as string;
+
+ if (folderPath === "") {
+ void DialogHost.showDialog({
+ id: "connectionFolderPath",
+ type: DialogType.Prompt,
+ title: "Create New Folder",
+ values: {
+ prompt: "Enter the name of the new folder to create:",
+ },
+ description: ["A connection folder is an organizational unit for connections."],
+ }).then((response) => {
+ if (response.closure === DialogResponseClosure.Accept && response.values) {
+ let newFolderName = response.values.input as string;
+ if (newFolderName.length === 0 || !newFolderName.startsWith("/")) {
+ newFolderName = "/" + newFolderName;
+ }
+ this.editorRef.current?.updateInputValue(newFolderName, "folderPath");
+ } else {
+ this.editorRef.current?.updateInputValue("/", "folderPath");
+ }
+ });
+ }
+ };
+
+ /**
+ * Loads all known folders from the database and creates a list of paths out of the hierarchical folder structure.
+ */
+ private loadKnownFolders = async (): Promise => {
+ this.knownPaths = [""];
+
+ const addPaths = async (parent: number, currentPath: string): Promise => {
+ const list = await ShellInterface.dbConnections.listFolderPaths(parent);
+ for (const entry of list) {
+ const newPath = (currentPath.length === 1 ? "" : currentPath) + "/" + entry.caption;
+ this.knownPaths.push(newPath);
+ await addPaths(entry.id, newPath);
+ }
+ };
+
+ const topLevel = await ShellInterface.dbConnections.listFolderPaths();
+ for (const entry of topLevel) {
+ this.knownPaths.push(entry.caption);
+ await addPaths(entry.id, entry.caption);
+ }
};
private setProgressMessage = (message: string): void => {
diff --git a/gui/frontend/src/modules/db-editor/ConnectionTab.tsx b/gui/frontend/src/modules/db-editor/ConnectionTab.tsx
index d29c71085..5a9be6bd6 100644
--- a/gui/frontend/src/modules/db-editor/ConnectionTab.tsx
+++ b/gui/frontend/src/modules/db-editor/ConnectionTab.tsx
@@ -39,7 +39,7 @@ import { IMdsChatData, IMdsChatStatus } from "../../communication/ProtocolMds.js
import { ResponseError } from "../../communication/ResponseError.js";
import { ChatOptionAction, ChatOptions, IChatOptionsState } from "../../components/Chat/ChatOptions.js";
import { IEditorPersistentState } from "../../components/ui/CodeEditor/CodeEditor.js";
-import { IScriptExecutionOptions } from "../../components/ui/CodeEditor/index.js";
+import { IScriptExecutionOptions, Range } from "../../components/ui/CodeEditor/index.js";
import { ComponentBase, IComponentProperties, IComponentState } from "../../components/ui/Component/ComponentBase.js";
import { SplitContainer } from "../../components/ui/SplitContainer/SplitContainer.js";
import { type ICdmConnectionEntry } from "../../data-models/ConnectionDataModel.js";
@@ -49,22 +49,25 @@ import {
} from "../../data-models/OpenDocumentDataModel.js";
import { QueryType } from "../../parsing/parser-common.js";
import { ExecutionContext } from "../../script-execution/ExecutionContext.js";
-import { SQLExecutionContext } from "../../script-execution/SQLExecutionContext.js";
+import { SQLExecutionContext, type IRuntimeErrorResult } from "../../script-execution/SQLExecutionContext.js";
import {
- IExecutionResult, INotebookFileFormat, IResponseDataOptions, ITextResultEntry, LoadingState, currentNotebookVersion,
+ IExecutionResult, INotebookFileFormat, IResponseDataOptions, ITextResultEntry, LoadingState,
+ currentNotebookVersion,
} from "../../script-execution/index.js";
+import { appParameters } from "../../supplement/AppParameters.js";
import {
IEditorExtendedExecutionOptions, IMrsDbObjectEditRequest, IMrsSchemaEditRequest, type IColumnDetails,
type IOpenFileDialogResult,
} from "../../supplement/RequisitionTypes.js";
import { requisitions } from "../../supplement/Requisitions.js";
-import { appParameters } from "../../supplement/AppParameters.js";
import { Settings } from "../../supplement/Settings/Settings.js";
import { RunMode, webSession } from "../../supplement/WebSession.js";
import {
EditorLanguage, IScriptRequest, ISqlPageRequest,
} from "../../supplement/index.js";
-import { convertErrorToString, resolvePageSize, saveTextAsFile, selectFile, uuid } from "../../utilities/helpers.js";
+import {
+ convertErrorToString, resolvePageSize, saveTextAsFile, selectFileInBrowser, uuid,
+} from "../../utilities/helpers.js";
import { formatTime, formatWithNumber } from "../../utilities/string-helpers.js";
import { getRouterPortForConnection } from "../mrs/mrs-helpers.js";
import { IMrsLoginResult } from "../mrs/sdk/MrsBaseClasses.js";
@@ -75,10 +78,10 @@ import { Notebook } from "./Notebook.js";
import { PerformanceDashboard } from "./PerformanceDashboard.js";
import { ScriptEditor } from "./ScriptEditor.js";
import { ServerStatus } from "./ServerStatus.js";
+import { SqlQueryExecutor } from "./SqlQueryExecutor.js";
import { IConsoleWorkerResultData, ScriptingApi } from "./console.worker-types.js";
import { ExecutionWorkerPool } from "./execution/ExecutionWorkerPool.js";
import { DocumentContext, ISavedGraphData, IToolbarItems } from "./index.js";
-import { SqlQueryExecutor } from "./SqlQueryExecutor.js";
interface IResultTimer {
timer: SetIntervalAsyncTimer;
@@ -310,11 +313,14 @@ Execute \\help or \\? for help;`;
}
private static shiftMLEStacktraceLineNumbers = (
- stackTrace: QueryResult, jsStartLine: number): string | undefined => {
+ stackTrace: QueryResult, jsStartLine: number): IRuntimeErrorResult | undefined => {
if (stackTrace?.rows && stackTrace.rows.length > 0) {
const stackTraceRow = stackTrace.rows[0][0];
if (stackTraceRow) {
+
+ let range!: Range;
+
let rowValue = stackTraceRow.split("\n").filter((val) => { return val !== ""; });
if (rowValue.length > 50) {
@@ -333,22 +339,34 @@ Execute \\help or \\? for help;`;
// Check if this is a multiline error
if (!rowInfo[1].includes("-")) {
rowInfo[1] = `${Number(rowInfo[1]) + jsStartLine}`;
+
+ range = new Range(
+ Number(rowInfo[1]),
+ Number(rowInfo[2].split("-")[0]),
+ Number(rowInfo[1]),
+ Number(rowInfo[2].split("-")[1]),
+ );
} else {
const multiLineError = rowInfo[1].split("-").map((val) => {
return `${Number(val) + jsStartLine}`;
});
rowInfo[1] = multiLineError.join("-");
+
+ range = new Range(
+ Number(rowInfo[1].split("-")[0]),
+ Number(rowInfo[2].split("-")[0]),
+ Number(rowInfo[1].split("-")[1]),
+ Number(rowInfo[2].split("-")[1]),
+ );
}
rowValue[index] = rowInfo.join(":");
}
- return `Exception Stack Trace: \n${rowValue.join("\n")}`.trim();
+ return { message: `Exception Stack Trace: \n${rowValue.join("\n")}`.trim(), range };
}
}
-
- return undefined;
};
public override componentDidMount(): void {
@@ -945,7 +963,7 @@ Execute \\help or \\? for help;`;
return requisitions.executeRemote("editorLoadNotebook", undefined);
}
- const selection = await selectFile([".mysql-notebook"], false);
+ const selection = await selectFileInBrowser([".mysql-notebook"], false);
if (selection) {
const file = selection[0];
const reader = new FileReader();
@@ -1080,7 +1098,7 @@ Execute \\help or \\? for help;`;
return;
}
- const { statementCount, errorCount, startTime, jsStartLine } = result;
+ const { statementCount, errorCount, startTime, jsStartLine, errorMessage, errorStatementIndex } = result;
if (this.mrsSdkUpdateRequired) {
// Enforce a refresh of the MRS Sdk Cache
@@ -1093,6 +1111,8 @@ Execute \\help or \\? for help;`;
// If MLE is enabled, collect all stack trace, console.log() output and all errors.
if (savedState.mleEnabled) {
const resultData: ITextResultEntry[] = [];
+ // Clear any existing runtime error decorations as they become obsolete after new execution
+ void context.clearRuntimeErrorData();
try {
const stackTrace: QueryResult = await connection?.backend?.execute(
@@ -1104,12 +1124,14 @@ Execute \\help or \\? for help;`;
resultData.push({
type: MessageType.Error,
index: -1,
- content: updatedStacktrace + "\n",
- language: "ini",
+ content: updatedStacktrace.message + "\n",
+ language: "ansi",
});
+ void context.addRuntimeErrorData(errorStatementIndex,
+ { message: errorMessage, range: updatedStacktrace.range });
}
} catch (error) {
- console.error("Error while getting stack trace:\n " + String(error));
+ console.error("Error while getting stack trace:\n" + String(error));
}
try {
@@ -1124,10 +1146,10 @@ Execute \\help or \\? for help;`;
const consoleLogInfo: string = `${row[0]}`.trim();
resultData.push({
- type: MessageType.Info,
+ type: MessageType.Log,
index: -1,
content: consoleLogInfo + "\n",
- language: "json",
+ language: "ansi",
});
}
}
@@ -1151,7 +1173,7 @@ Execute \\help or \\? for help;`;
type: MessageType.Warning,
index: -1,
content: consoleErrorInfo + "\n",
- language: "json",
+ language: "ansi",
});
}
}
@@ -2272,6 +2294,7 @@ Execute \\help or \\? for help;`;
break;
}
+
case ChatOptionAction.LoadChatOptions: {
if (appParameters.embedded) {
const options = {
@@ -2291,6 +2314,7 @@ Execute \\help or \\? for help;`;
break;
}
+
case ChatOptionAction.StartNewChat: {
// Start new chat but keep schemaName and modelId
this.updateChatOptionsState({
diff --git a/gui/frontend/src/modules/db-editor/DocumentModule.tsx b/gui/frontend/src/modules/db-editor/DocumentModule.tsx
index 17937c857..ffaf13310 100644
--- a/gui/frontend/src/modules/db-editor/DocumentModule.tsx
+++ b/gui/frontend/src/modules/db-editor/DocumentModule.tsx
@@ -28,15 +28,15 @@ import { Component, ComponentChild, createRef } from "preact";
import { ICodeEditorModel, type IEditorPersistentState } from "../../components/ui/CodeEditor/CodeEditor.js";
import { CodeEditorMode, Monaco } from "../../components/ui/CodeEditor/index.js";
import { ExecutionContexts } from "../../script-execution/ExecutionContexts.js";
-import { requisitions } from "../../supplement/Requisitions.js";
import { appParameters } from "../../supplement/AppParameters.js";
+import { requisitions } from "../../supplement/Requisitions.js";
import {
IMrsAuthAppEditRequest, IMrsContentSetEditRequest, IMrsDbObjectEditRequest, IMrsSchemaEditRequest,
IMrsSdkExportRequest, IMrsUserEditRequest, InitialEditor, type IDocumentOpenData,
} from "../../supplement/RequisitionTypes.js";
import { Settings } from "../../supplement/Settings/Settings.js";
import {
- DBConnectionEditorType, DBType, IConnectionDetails, type IShellSessionDetails,
+ ConnectionEditorType, DBType, IConnectionDetails, type IShellSessionDetails,
} from "../../supplement/ShellInterface/index.js";
import { ConnectionBrowser } from "./ConnectionBrowser.js";
import {
@@ -47,7 +47,9 @@ import {
} from "./index.js";
import { ApplicationDB, StoreType } from "../../app-logic/ApplicationDB.js";
-import { IMySQLConnectionOptions } from "../../communication/MySQL.js";
+import {
+ IMySQLConnectionOptions,
+} from "../../communication/MySQL.js";
import { IMrsServiceData } from "../../communication/ProtocolMrs.js";
import { Button } from "../../components/ui/Button/Button.js";
import { ComponentPlacement } from "../../components/ui/Component/ComponentBase.js";
@@ -66,7 +68,7 @@ import { EditorLanguage, IExecutionContext, INewEditorRequest } from "../../supp
import { ShellInterface } from "../../supplement/ShellInterface/ShellInterface.js";
import { ShellInterfaceSqlEditor } from "../../supplement/ShellInterface/ShellInterfaceSqlEditor.js";
import { RunMode, webSession } from "../../supplement/WebSession.js";
-import { convertErrorToString, loadFileAsText, selectFile, uuid } from "../../utilities/helpers.js";
+import { convertErrorToString, loadFileAsText, selectFileInBrowser, uuid } from "../../utilities/helpers.js";
import { MrsHub } from "../mrs/MrsHub.js";
import { ExecutionWorkerPool } from "./execution/ExecutionWorkerPool.js";
@@ -80,7 +82,8 @@ import {
import { ui } from "../../app-logic/UILayer.js";
import {
- CdmEntityType, ConnectionDataModel, type ConnectionDataModelEntry, type ICdmConnectionEntry,
+ CdmEntityType, ConnectionDataModel, type ConnectionDataModelEntry,
+ type ICdmConnectionEntry,
} from "../../data-models/ConnectionDataModel.js";
import type { Command } from "../../data-models/data-model-types.js";
import {
@@ -102,11 +105,12 @@ import { ShellInterfaceShellSession } from "../../supplement/ShellInterface/Shel
import { ShellPromptHandler } from "../common/ShellPromptHandler.js";
import type { IShellEditorModel } from "../shell/index.js";
import { ShellTab, type IShellTabPersistentState } from "../shell/ShellTab.js";
+import { ConnectionDataModelListener } from "./ConnectionDataModelListener.js";
import { LakehouseNavigatorTab } from "./LakehouseNavigator.js";
import { SidebarCommandHandler } from "./SidebarCommandHandler.js";
import { SimpleEditor } from "./SimpleEditor.js";
import { sendSqlUpdatesFromModel } from "./SqlQueryExecutor.js";
-import { ConnectionDataModelListener } from "./ConnectionDataModelListener.js";
+import { ConnectionProcessor } from "../common/ConnectionProcessor.js";
/**
* Details generated while adding a new connection tab. These are used in the render method to fill the tab
@@ -188,6 +192,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
private connectionPresentation: Map = new Map();
private workerPool: ExecutionWorkerPool;
+ private connectionsDataModel: ConnectionDataModel;
private latestPagesByConnection: Map = new Map();
private maxConnectionDocumentSuffix: Map = new Map();
@@ -205,14 +210,15 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
private mrsHubRef = createRef();
private currentTabRef = createRef();
- private connectionsDataModel: ConnectionDataModel;
private documentDataModel: OpenDocumentDataModel;
private ociDataModel: OciDataModel;
private shellTaskDataModel: ShellTaskDataModel;
private dataModelListener: ConnectionDataModelListener;
- #sidebarCommandHandler: SidebarCommandHandler;
+ private sidebarCommandHandler: SidebarCommandHandler;
+
+ private autoLogoutTimer?: ReturnType;
public constructor(props: {}) {
super(props);
@@ -240,7 +246,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
this.setState({ loading: false });
});
- this.#sidebarCommandHandler = new SidebarCommandHandler(this.connectionsDataModel, this.mrsHubRef);
+ this.sidebarCommandHandler = new SidebarCommandHandler(this.connectionsDataModel, this.mrsHubRef);
this.state = {
selectedPage: "",
@@ -309,9 +315,26 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
if (this.containerRef.current && !appParameters.embedded) {
this.containerRef.current.addEventListener("keydown", this.handleKeyPress);
}
+
+ // A user is logged in when this component gets mounted, so we can start the auto logout timer.
+ if (webSession.runMode === RunMode.SingleServer) {
+ this.restartAutologoutTimer();
+ ["click", "mousemove", "keydown", "scroll", "touchstart"].forEach((evt) => {
+ window.addEventListener(evt, this.restartAutologoutTimer, true);
+ });
+ }
}
public override componentWillUnmount(): void {
+ clearTimeout(this.autoLogoutTimer);
+ this.autoLogoutTimer = undefined;
+
+ if (webSession.runMode === RunMode.SingleServer) {
+ ["click", "mousemove", "keydown", "scroll", "touchstart"].forEach((evt) => {
+ window.removeEventListener(evt, this.restartAutologoutTimer, true);
+ });
+ }
+
if (this.containerRef.current && !appParameters.embedded) {
this.containerRef.current.removeEventListener("keydown", this.handleKeyPress);
}
@@ -751,8 +774,8 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
);
}
- public handlePushConnection = (entry: ICdmConnectionEntry): void => {
- this.connectionsDataModel.addConnectionEntry(entry);
+ public handlePushConnection = async (entry: ICdmConnectionEntry): Promise => {
+ await this.connectionsDataModel.addConnectionEntry(entry);
};
private handleKeyPress = (event: KeyboardEvent): void => {
@@ -766,28 +789,25 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
};
private handleAddConnection = (entry: ICdmConnectionEntry): void => {
- ShellInterface.dbConnections.addDbConnection(webSession.currentProfileId, entry.details)
- .then((connection) => {
- if (connection !== undefined) {
- entry.details.id = connection[0];
- this.connectionsDataModel.addConnectionEntry(entry);
+ void ConnectionProcessor.addConnection(entry, this.connectionsDataModel);
+ };
- requisitions.executeRemote("connectionAdded", entry.details);
+ private handleUpdateConnection = (entry: ICdmConnectionEntry): void => {
+ void this.connectionsDataModel.groupFromPath(entry.details.folderPath).then((group) => {
+ const id = group.folderPath.id;
+ ShellInterface.dbConnections.updateDbConnection(webSession.currentProfileId, entry.details, id).then(() => {
+ // Connection groups may have changed.
+ void entry.parent?.refresh?.(); // Old parent.
+ requisitions.executeRemote("refreshConnectionGroup", entry.parent?.folderPath.id);
+ if (entry.parent !== group) {
+ void group.refresh?.(); // New parent.
+ requisitions.executeRemote("refreshConnectionGroup", group.parent?.folderPath.id);
}
+ requisitions.executeRemote("connectionUpdated", entry.details);
}).catch((reason) => {
const message = convertErrorToString(reason);
- void ui.showErrorMessage(`Cannot add DB connection: ${message}`, {});
-
+ void requisitions.execute("showError", "Cannot update DB connection: " + message);
});
- };
-
- private handleUpdateConnection = (entry: ICdmConnectionEntry): void => {
- ShellInterface.dbConnections.updateDbConnection(webSession.currentProfileId, entry.details).then(() => {
- this.connectionsDataModel.updateConnectionDetails(entry);
- requisitions.executeRemote("connectionUpdated", entry.details);
- }).catch((reason) => {
- const message = convertErrorToString(reason);
- void ui.showErrorMessage(`Cannot update DB connection: ${message}`, {});
});
};
@@ -799,8 +819,8 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
requisitions.executeRemote("connectionRemoved", entry.details);
}).catch((reason) => {
- const message = convertErrorToString(reason);
- void ui.showErrorMessage(`Cannot remove DB connection: ${message}`, {});
+ const message = reason instanceof Error ? reason.message : String(reason);
+ void requisitions.execute("showError", "Cannot remove DB connection: " + message);
});
};
@@ -1166,7 +1186,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
};
private doLogout = async (): Promise => {
- this.connectionsDataModel.clear();
+ await this.connectionsDataModel.clear();
this.documentDataModel.clear();
this.ociDataModel.clear();
webSession.clearCredentials();
@@ -1177,6 +1197,23 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
return requisitions.execute("userLoggedOut", {});
};
+ private handleImportWorkbenchConnections = async (): Promise => {
+ const files: File[] | null = await selectFileInBrowser([".xml"], false);
+ if (files && files.length > 0) {
+ const file = files[0];
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ const xmlString = e.target?.result?.toString() ?? "";
+ void ConnectionProcessor.importMySQLWorkbenchConnections(xmlString, this.connectionsDataModel);
+ };
+
+ reader.readAsText(file);
+ }
+
+ return true;
+ };
+
/**
* Activates the tab for the given connection. If the tab already exists it is simply activated, otherwise a new
* tab is created.
@@ -1205,8 +1242,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
await this.addNewTab(newEntry, suppressAbout, initialEditor, pageId);
selectedPage = pageId;
} catch (error) {
- const message = convertErrorToString(error);
- void ui.showErrorMessage(message, {});
+ void ui.showErrorMessage(convertErrorToString(error), {});
} finally {
this.hideProgress(true);
}
@@ -1258,7 +1294,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
const entryId = uuid();
let useNotebook;
if (connection.details.settings && connection.details.settings.defaultEditor) {
- useNotebook = connection.details.settings.defaultEditor === DBConnectionEditorType.DbNotebook;
+ useNotebook = connection.details.settings.defaultEditor === ConnectionEditorType.DbNotebook;
} else {
useNotebook = Settings.get("dbEditor.defaultEditor", "notebook") === "notebook";
}
@@ -1414,9 +1450,11 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
state.shellSessionTabs?.forEach((info) => {
remainingPageIds.push(info.dataModelEntry.id);
});
+
state.connectionTabs?.forEach((info) => {
remainingPageIds.push(info.dataModelEntry.id);
});
+
state.documentTabs?.forEach((info) => {
remainingPageIds.push(info.dataModelEntry.id);
});
@@ -1454,8 +1492,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
/**
* Completely removes a tab, with all its editors (if the tab is a connection tab).
*
- * @param tabIds The list of tab IDs to remove. For standalone documents this is equal to the document ID.
- *
+ * @param tabIds tabIds The list of tab IDs to remove. For standalone documents this is equal to the document ID.
* @returns A promise resolving to true, when the tab removal is finished.
*/
private removeTabs = async (tabIds: string[]): Promise => {
@@ -1482,6 +1519,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
remainingTabsState.shellSessionTabs.push(info);
}
});
+
connectionTabs.forEach((info) => {
if (tabIds.includes(info.dataModelEntry.id)) {
closingTabs.connectionTabs.push(info);
@@ -1489,6 +1527,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
remainingTabsState.connectionTabs.push(info);
}
});
+
documentTabs.forEach((info) => {
if (tabIds.includes(info.dataModelEntry.id)) {
closingTabs.documentTabs.push(info);
@@ -1505,9 +1544,11 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
await Promise.all(closingTabs.shellSessionTabs.map(async (info) => {
await this.removeShellTab(info.dataModelEntry.id, false);
}));
+
await Promise.all(closingTabs.connectionTabs.map(async (info) => {
await this.removeConnectionTab(info);
}));
+
closingTabs.documentTabs.forEach((info) => {
this.removeDocument(info.dataModelEntry.id, info.dataModelEntry.id);
});
@@ -1570,7 +1611,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
};
/**
- * Similar to `removeTabs`, but for shell session tabs.
+ * Similar to `removeTab`, but for shell session tabs.
*
* @param id The session (tab) id to remove.
* @param updateAppState Whether application state and tabs have to be updated, defaults to true.
@@ -1578,23 +1619,41 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
* @returns A promise resolving to true, when the tab removal is finished.
*/
private removeShellTab = async (id: string, updateAppState = true): Promise => {
- const { shellSessionTabs } = this.state;
+ const { selectedPage, shellSessionTabs } = this.state;
if (updateAppState) {
// Remove all result data from the application DB.
void ApplicationDB.removeDataByTabId(StoreType.Shell, id);
}
- const session = shellSessionTabs.find((info) => {
+ const index = shellSessionTabs.findIndex((info) => {
return info.dataModelEntry.id === id;
});
- if (session) {
+ if (index > -1) {
this.documentDataModel.removeShellSession(undefined, id);
+ const session = shellSessionTabs[index];
await session.savedState.backend.closeShellSession();
+ shellSessionTabs.splice(index, 1);
requisitions.executeRemote("sessionRemoved", session.dataModelEntry.details);
+
+ let newSelection = selectedPage;
+
+ if (id === newSelection) {
+ if (index > 0) {
+ newSelection = shellSessionTabs[index - 1].dataModelEntry.id;
+ } else {
+ if (index >= shellSessionTabs.length - 1) {
+ newSelection = "sessions"; // The overview page cannot be closed.
+ } else {
+ newSelection = shellSessionTabs[index + 1].dataModelEntry.id;
+ }
+ }
+ }
+
+ this.setState({ selectedPage: newSelection, shellSessionTabs });
}
if (updateAppState) {
@@ -1602,9 +1661,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
return t.dataModelEntry.id !== id;
});
- this.updateAppTabsState({
- shellSessionTabs: remainingTabs,
- }, [id]);
+ this.updateAppTabsState({ shellSessionTabs: remainingTabs }, [id]);
}
return true;
@@ -1983,6 +2040,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
const connectionState = tab ? this.connectionPresentation.get(tab.dataModelEntry) : undefined;
if (connectionState) {
this.latestPagesByConnection.set(tab!.connection.details.id, details.tabId);
+
// Check if we have an open document with the new id
// (administration pages like server status count here too).
const newEditor = connectionState.documentStates.find((candidate: IOpenDocumentState) => {
@@ -1997,9 +2055,12 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
// Must be an administration page or an external script then.
if (details.document.type === OdmEntityType.Script) {
- const newState = this.addEditorFromScript(details.tabId, tab!.connection,
- details.document, details.content ?? "");
- connectionState.documentStates.push(newState);
+ // External scripts come with their full content.
+ if (details.content !== undefined) {
+ const newState = this.addEditorFromScript(details.tabId, tab!.connection,
+ details.document, details.content);
+ connectionState.documentStates.push(newState);
+ }
} else {
const document = details.document as IOdmAdminEntry;
const newState: IOpenDocumentState = {
@@ -2141,7 +2202,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
}
default: {
- return this.#sidebarCommandHandler.handleDocumentCommand(command, entry);
+ return this.sidebarCommandHandler.handleDocumentCommand(command, entry);
}
}
@@ -2161,40 +2222,78 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
qualifiedName?: QualifiedName): Promise => {
const { connectionTabs } = this.state;
+ // First handle commands which need no entry.
+ if (!entry) {
+ switch (command.command) {
+ case "addConsole": {
+ void this.activateShellTab(uuid());
+
+ return { success: true };
+ }
+
+ case "msg.logOut": {
+ await this.doLogout();
+
+ return { success: true };
+ }
+
+ case "msg.importWorkbenchConnections": {
+ await this.handleImportWorkbenchConnections();
+
+ return { success: true };
+ }
+
+ default: {
+ return this.sidebarCommandHandler.handleConnectionTreeCommand(command, entry, qualifiedName);
+ }
+ }
+ }
+
const canClose = this.currentTabRef.current ? (await this.currentTabRef.current.canClose()) : true;
if (!canClose) {
return { success: false };
}
- const connection = entry?.connection;
- const connectionId = connection?.details.id;
- let pageId: string | undefined;
- if (connection) {
- pageId = this.resolveLatestPageId(connection);
- }
-
- switch (command.command) {
- case "msg.openConnection": {
- if (connection) {
- const initialEditor = (command.arguments && command.arguments.length > 0)
- ? command.arguments[0]
- : undefined;
- await this.showPage({ connectionId, pageId, force: true, editor: initialEditor as InitialEditor });
+ if (entry.type === CdmEntityType.ConnectionGroup) {
+ switch (command.command) {
+ default: {
+ return this.sidebarCommandHandler.handleConnectionTreeCommand(command, entry, qualifiedName);
}
-
- break;
+ }
+ } else {
+ const connection = entry.connection;
+ const connectionId = connection?.details.id;
+ let pageId: string | undefined;
+ if (connection) {
+ pageId = this.resolveLatestPageId(connection);
}
- case "msg.loadScriptFromDisk": {
- if (connection) {
- await this.loadScriptFromDisk(connection.details, pageId);
+ switch (command.command) {
+ case "msg.openConnection": {
+ if (connection) {
+ const initialEditor = (command.arguments && command.arguments.length > 0)
+ ? command.arguments[0]
+ : undefined;
+ await this.showPage({
+ connectionId,
+ pageId,
+ force: true,
+ editor: initialEditor as InitialEditor,
+ });
+ }
+
+ break;
}
- break;
- }
+ case "msg.loadScriptFromDisk": {
+ if (connection) {
+ await this.loadScriptFromDisk(connection.details, pageId);
+ }
- case "setCurrentSchemaMenuItem": {
- if (connection) {
+ break;
+ }
+
+ case "setCurrentSchemaMenuItem": {
const tab = connectionTabs.find((tab) => {
return tab.connection.details.id === connection.details.id;
});
@@ -2204,31 +2303,32 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
id: tab.dataModelEntry.id, connectionId: connection.details.id, schema: qualifiedName.name!,
});
}
- }
- break;
- }
+ break;
+ }
- case "msg.newSessionUsingConnection": {
- await this.activateShellTab(uuid(), connection?.details.id, connection?.details.caption);
+ case "msg.newSessionUsingConnection": {
+ await this.activateShellTab(uuid(), connection?.details.id, connection?.details.caption);
- break;
- }
+ break;
+ }
- case "addConsole": {
- void this.activateShellTab(uuid());
+ case "addConsole": {
+ void this.activateShellTab(uuid());
- break;
- }
+ break;
+ }
- case "msg.logOut": {
- await this.doLogout();
+ case "msg.logOut": {
+ await this.doLogout();
- break;
- }
+ break;
+ }
- default: {
- return this.#sidebarCommandHandler.handleConnectionTreeCommand(command, entry, qualifiedName, pageId);
+ default: {
+ return this.sidebarCommandHandler.handleConnectionTreeCommand(command, entry, qualifiedName,
+ pageId);
+ }
}
}
@@ -2317,7 +2417,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
}
default: {
- return this.#sidebarCommandHandler.handleOciCommand(command, entry);
+ return this.sidebarCommandHandler.handleOciCommand(command, entry);
}
}
@@ -2325,7 +2425,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
};
private async loadScriptFromDisk(connection: IConnectionDetails, pageId?: string): Promise {
- const files = await selectFile([".sql"], false);
+ const files = await selectFileInBrowser([".sql"], false);
if (files && files.length > 0) {
const file = files[0];
const content = await loadFileAsText(file);
@@ -2449,7 +2549,7 @@ export class DocumentModule extends Component<{}, IDocumentModuleState> {
requisitions.executeRemote("sessionAdded", document.details);
} catch (error) {
const message = convertErrorToString(error);
- void ui.showErrorMessage(`Shell Session Error: ${message}`, {});
+ void ui.showErrorMessage("Shell Session Error: " + message, {});
}
this.hideProgress(true);
@@ -2891,4 +2991,33 @@ EXAMPLES
return caption;
}
+
+ private restartAutologoutTimer = () => {
+ clearTimeout(this.autoLogoutTimer);
+ this.autoLogoutTimer = undefined;
+
+ let logoutTimout: number | undefined;
+ const testTimeout = process.env.TEST_AUTO_LOGOUT_TIMEOUT;
+ if (testTimeout !== undefined) {
+ logoutTimout = parseInt(testTimeout, 10); // Must be in milliseconds.
+
+ if (logoutTimout === undefined || isNaN(logoutTimout)) {
+ logoutTimout = undefined;
+ }
+ }
+
+ if (logoutTimout === undefined) {
+ // Use the configured auto logout timeout. Convert hours to milliseconds.
+ logoutTimout = Settings.get("general.autoLogoutTimeout", 12) * 3600 * 1000;
+ }
+
+ if (logoutTimout > 0) {
+ this.autoLogoutTimer = setTimeout(() => {
+ void ui.showInformationMessage("You have been logged out due to inactivity. Please log in again.",
+ { modal: true }, "OK");
+ void this.doLogout();
+ }, logoutTimout);
+ }
+ };
+
}
diff --git a/gui/frontend/src/modules/db-editor/DocumentSideBar/DocumentSideBar.tsx b/gui/frontend/src/modules/db-editor/DocumentSideBar/DocumentSideBar.tsx
index 884d5c427..a5533adb8 100644
--- a/gui/frontend/src/modules/db-editor/DocumentSideBar/DocumentSideBar.tsx
+++ b/gui/frontend/src/modules/db-editor/DocumentSideBar/DocumentSideBar.tsx
@@ -45,8 +45,9 @@ import { IMenuItemProperties, MenuItem } from "../../../components/ui/Menu/MenuI
import { ISplitterPaneSizeInfo } from "../../../components/ui/SplitContainer/SplitContainer.js";
import { ITreeGridOptions, SetDataAction, TreeGrid } from "../../../components/ui/TreeGrid/TreeGrid.js";
import {
- CdmEntityType, type ConnectionDataModelEntry, type ICdmConnectionEntry, type ICdmRestAuthAppEntry,
- type ICdmRestRootEntry, type ICdmRestSchemaEntry, type ICdmRestServiceEntry,
+ CdmEntityType, type ConnectionDMActionList, type ConnectionDataModelEntry, type ConnectionDataModelNoGroupEntry,
+ type ICdmConnectionEntry, type ICdmConnectionGroupEntry, type ICdmRestAuthAppEntry, type ICdmRestRootEntry,
+ type ICdmRestSchemaEntry, type ICdmRestServiceEntry,
} from "../../../data-models/ConnectionDataModel.js";
import {
OciDmEntityType, type IOciDmCompartment, type IOciDmProfile, type OciDataModelEntry,
@@ -69,6 +70,7 @@ import {
DocumentContext, type DocumentContextType, type IBaseTreeItem, type IConnectionTreeItem,
type IDocumentTreeItem, type IOciTreeItem, type ISideBarCommandResult, type QualifiedName,
} from "../index.js";
+import { Settings } from "../../../supplement/Settings/Settings.js";
/** Lookup for icons for a specific document type. */
export const documentTypeToFileIcon = new Map([
@@ -82,7 +84,7 @@ export const documentTypeToFileIcon = new Map(
]);
/** Mapping of connection data model types to icons (main types). */
-const cdmTypeToEntryIcon: Map = new Map([
+const cdmTypeToEntryIcon = new Map([
[CdmEntityType.Schema, Assets.db.schemaIcon],
[CdmEntityType.Table, Assets.db.tableIcon],
[CdmEntityType.View, Assets.db.viewIcon],
@@ -96,6 +98,7 @@ const cdmTypeToEntryIcon: Map = new Map([
[CdmEntityType.ForeignKey, Assets.db.foreignKeyIcon],
[CdmEntityType.TableGroup, Assets.db.tablesIcon],
[CdmEntityType.SchemaGroup, Assets.db.schemaIcon],
+ [CdmEntityType.ConnectionGroup, Assets.file.folderIcon],
[CdmEntityType.Admin, Assets.documents.adminDashboardIcon],
[CdmEntityType.MrsRoot, Assets.mrs.mainIcon],
[CdmEntityType.MrsService, Assets.mrs.serviceIcon],
@@ -199,6 +202,8 @@ interface IDataModelBaseEntry {
id: string;
caption: string,
state: IDataModelEntryState;
+ type: CdmEntityType | OdmEntityType | OciDmEntityType;
+ parent?: IDataModelBaseEntry;
refresh?: () => Promise;
getChildren?(): IDataModelBaseEntry[];
@@ -230,6 +235,7 @@ export class DocumentSideBar extends ComponentBase>([
+ [CdmEntityType.ConnectionGroup, createRef