gui/frontend/src/modules/db-editor/DocumentSideBar/DocumentSideBar.tsx (2,852 lines of code) (raw):

/* * Copyright (c) 2024, 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 "./DocumentSideBar.css"; import { ComponentChild, createRef, render, type RefObject } from "preact"; import { CellComponent, ColumnDefinition, RowComponent } from "tabulator-tables"; import { ui } from "../../../app-logic/UILayer.js"; import { DbSystem, LoadBalancer } from "../../../communication/Oci.js"; import { Accordion, IAccordionProperties, type IAccordionSection } from "../../../components/ui/Accordion/Accordion.js"; import type { AccordionSection } from "../../../components/ui/Accordion/AccordionSection.js"; import { Button } from "../../../components/ui/Button/Button.js"; import { Codicon } from "../../../components/ui/Codicon.js"; import { ComponentBase, ComponentPlacement, IComponentProperties, IComponentState, SelectionType, } from "../../../components/ui/Component/ComponentBase.js"; import { Container, Orientation } from "../../../components/ui/Container/Container.js"; import { Icon, type IIconOverlay } from "../../../components/ui/Icon/Icon.js"; import { Label } from "../../../components/ui/Label/Label.js"; import { Menu } from "../../../components/ui/Menu/Menu.js"; import { IMenuItemProperties, MenuItem } from "../../../components/ui/Menu/MenuItem.js"; 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, } from "../../../data-models/ConnectionDataModel.js"; import { OciDmEntityType, type IOciDmCompartment, type IOciDmProfile, type OciDataModelEntry, } from "../../../data-models/OciDataModel.js"; import { OdmEntityType, type OpenDocumentDataModelEntry } from "../../../data-models/OpenDocumentDataModel.js"; import { systemSchemas, type AdminPageType, type Command, type IDataModelEntryState, type ISubscriberActionType, } from "../../../data-models/data-model-types.js"; import { BastionLifecycleState } from "../../../oci-typings/oci-bastion/lib/model/bastion-lifecycle-state.js"; import { Assets } from "../../../supplement/Assets.js"; import { appParameters, requisitions } from "../../../supplement/Requisitions.js"; import { DBType } from "../../../supplement/ShellInterface/index.js"; import { RunMode, webSession } from "../../../supplement/WebSession.js"; import { EditorLanguage } from "../../../supplement/index.js"; import { convertErrorToString, uuid } from "../../../utilities/helpers.js"; import { EnabledState } from "../../mrs/mrs-helpers.js"; import { MrsDbObjectType } from "../../mrs/types.js"; import { DocumentContext, type DocumentContextType, type IBaseTreeItem, type IConnectionTreeItem, type IDocumentTreeItem, type IOciTreeItem, type ISideBarCommandResult, type QualifiedName, } from "../index.js"; /** Lookup for icons for a specific document type. */ export const documentTypeToFileIcon = new Map<EditorLanguage, string | Codicon>([ ["javascript", Assets.file.javascriptIcon], ["mysql", Assets.file.mysqlIcon], ["sql", Assets.file.sqliteIcon], ["python", Assets.file.pythonIcon], ["msg", Assets.documents.notebookIcon], ["typescript", Assets.file.typescriptIcon], ["json", Codicon.Json], ]); /** Mapping of connection data model types to icons (main types). */ const cdmTypeToEntryIcon: Map<CdmEntityType, string> = new Map([ [CdmEntityType.Schema, Assets.db.schemaIcon], [CdmEntityType.Table, Assets.db.tableIcon], [CdmEntityType.View, Assets.db.viewIcon], [CdmEntityType.View, Assets.db.viewIcon], [CdmEntityType.StoredFunction, Assets.db.functionIcon], [CdmEntityType.StoredProcedure, Assets.db.procedureIcon], [CdmEntityType.Event, Assets.db.eventIcon], [CdmEntityType.Trigger, Assets.db.triggerIcon], [CdmEntityType.Column, Assets.db.columnNullableIcon], [CdmEntityType.Index, Assets.db.indexIcon], [CdmEntityType.ForeignKey, Assets.db.foreignKeyIcon], [CdmEntityType.TableGroup, Assets.db.tablesIcon], [CdmEntityType.SchemaGroup, Assets.db.schemaIcon], [CdmEntityType.Admin, Assets.documents.adminDashboardIcon], [CdmEntityType.MrsRoot, Assets.mrs.mainIcon], [CdmEntityType.MrsService, Assets.mrs.serviceIcon], [CdmEntityType.MrsSchema, Assets.mrs.schemaIcon], [CdmEntityType.MrsContentSet, Assets.mrs.contentSetIcon], [CdmEntityType.MrsUser, Assets.oci.profileIcon], [CdmEntityType.MrsAuthApp, Assets.mrs.authAppIcon], [CdmEntityType.MrsServiceAuthApp, Assets.mrs.authAppIcon], [CdmEntityType.MrsAuthAppGroup, Assets.mrs.authAppsIcon], [CdmEntityType.MrsContentFile, Assets.mrs.contentFileIcon], [CdmEntityType.MrsDbObject, Assets.mrs.dbObjectIcon], [CdmEntityType.MrsRouter, Assets.router.routerIcon], [CdmEntityType.MrsRouterGroup, Assets.router.routersIcon], [CdmEntityType.MrsRouterService, Assets.mrs.serviceIcon], ]); /** Mapping of connection data model types to icons (sub types). */ const cdmSubTypeToDbObjectIcon: Map<CdmEntityType, string> = new Map([ [CdmEntityType.Table, Assets.db.tablesIcon], [CdmEntityType.View, Assets.db.viewsIcon], [CdmEntityType.StoredFunction, Assets.db.functionsIcon], [CdmEntityType.StoredProcedure, Assets.db.proceduresIcon], [CdmEntityType.Event, Assets.db.eventsIcon], [CdmEntityType.Trigger, Assets.db.triggersIcon], [CdmEntityType.Column, Assets.db.columnsIcon], [CdmEntityType.Index, Assets.db.indexesIcon], [CdmEntityType.ForeignKey, Assets.db.foreignKeysIcon], ]); /** Standard mapping from document types to their icons. */ const odmTypeToDocumentIcon = new Map<OdmEntityType, string>([ [OdmEntityType.ConnectionPage, Assets.db.mysqlConnectionIcon], [OdmEntityType.Overview, Assets.documents.overviewPageIcon], [OdmEntityType.Notebook, Assets.documents.notebookIcon], [OdmEntityType.Script, Assets.file.javascriptIcon], [OdmEntityType.AdminPage, Assets.documents.adminDashboardIcon], [OdmEntityType.ShellSessionRoot, Assets.documents.sessionIcon], [OdmEntityType.ShellSession, Assets.documents.sessionIcon], ]); /** Lookup for icons for special pages. */ export const pageTypeToDocumentIcon: Map<AdminPageType, string> = new Map([ ["serverStatus", Assets.documents.adminServerStatusIcon], ["clientConnections", Assets.documents.clientConnectionsIcon], ["performanceDashboard", Assets.documents.adminPerformanceDashboardIcon], ["lakehouseNavigator", Assets.lakehouse.navigatorIcon], ]); /** Standard mapping from OCI types to their icons. */ const ociTypeToEntryIcon: Map<OciDmEntityType, string> = new Map([ [OciDmEntityType.ConfigurationProfile, Assets.oci.profileIcon], [OciDmEntityType.Compartment, Assets.file.folderIcon], [OciDmEntityType.Bastion, Assets.oci.bastionIcon], [OciDmEntityType.ComputeInstance, Assets.oci.computeIcon], [OciDmEntityType.DbSystem, Assets.oci.dbSystemIcon], [OciDmEntityType.HeatWaveCluster, Assets.oci.dbSystemHWIcon], [OciDmEntityType.LoadBalancer, Assets.oci.loadBalancerIcon], ]); /** Mapping MRS DB object sub types to their icons. */ const mrsDbObjectTypeToIcon: Map<MrsDbObjectType, string> = new Map([ [MrsDbObjectType.Table, Assets.mrs.dbObjectTableIcon], [MrsDbObjectType.View, Assets.mrs.dbObjectViewIcon], [MrsDbObjectType.Procedure, Assets.mrs.dbObjectProcedureIcon], [MrsDbObjectType.Function, Assets.mrs.dbObjectFunctionIcon], //[MrsDbObjectType.Event, Assets.mrs.dbObjectEventIcon], ]); export interface IDocumentSideBarSectionState { expanded?: boolean; size?: number; } interface IDocumentSideBarProperties extends IComponentProperties { /** Which document item is currently selected. */ selectedOpenDocument: string; /** Which schema in the connection tree should be marked as being the current schema? */ markedSchema: string; /** The state of each accordion section. */ savedSectionState?: Map<string, IDocumentSideBarSectionState>; overviewId: string; onSelectConnectionItem?: (entry: ConnectionDataModelEntry) => Promise<void>; onSelectDocumentItem?: (entry: OpenDocumentDataModelEntry) => Promise<void>; onChangeItem?: (id: string, newCaption: string) => void; onSaveState?: (state: Map<string, IDocumentSideBarSectionState>) => void; onConnectionTreeCommand: (command: Command, entry?: ConnectionDataModelEntry, qualifiedName?: QualifiedName) => Promise<ISideBarCommandResult>; onDocumentTreeCommand: (command: Command, entry: OpenDocumentDataModelEntry) => Promise<ISideBarCommandResult>; onOciTreeCommand: (command: Command, entry: OciDataModelEntry) => Promise<ISideBarCommandResult>; } /** * Properties that are available in all data model entries. Used only for refresh methods that work for all * data trees the same way. */ interface IDataModelBaseEntry { id: string; caption: string, state: IDataModelEntryState; refresh?: () => Promise<unknown>; getChildren?(): IDataModelBaseEntry[]; } /** The type of the function that generates new tree items. */ type TreeItemGenerator<T extends IDataModelBaseEntry> = (entry: T) => IBaseTreeItem<T>; interface ISideBarTreeItems { openDocumentTreeItems: IDocumentTreeItem[]; connectionTreeItems: IConnectionTreeItem[]; ociTreeItems: IOciTreeItem[]; } interface IDocumentSideBarState extends IComponentState { treeItems: ISideBarTreeItems; /** If editing an editor's caption is active then this field holds its id. */ editing?: string; /** Keeps the new caption of an editor while it is being edited. */ tempCaption?: string; /** Set when the default connection is being opened (in single server mode). */ defaultOpening: boolean; } export class DocumentSideBar extends ComponentBase<IDocumentSideBarProperties, IDocumentSideBarState> { public static override contextType = DocumentContext; private cdmTypeToMenuRefMap = new Map<CdmEntityType, RefObject<Menu>>([ [CdmEntityType.Connection, createRef<Menu>()], [CdmEntityType.Schema, createRef<Menu>()], [CdmEntityType.Table, createRef<Menu>()], [CdmEntityType.Column, createRef<Menu>()], [CdmEntityType.View, createRef<Menu>()], [CdmEntityType.Trigger, createRef<Menu>()], [CdmEntityType.Index, createRef<Menu>()], [CdmEntityType.ForeignKey, createRef<Menu>()], [CdmEntityType.Event, createRef<Menu>()], [CdmEntityType.StoredProcedure, createRef<Menu>()], [CdmEntityType.StoredFunction, createRef<Menu>()], [CdmEntityType.MrsRoot, createRef<Menu>()], [CdmEntityType.MrsService, createRef<Menu>()], [CdmEntityType.MrsSchema, createRef<Menu>()], [CdmEntityType.MrsAuthAppGroup, createRef<Menu>()], [CdmEntityType.MrsAuthApp, createRef<Menu>()], [CdmEntityType.MrsServiceAuthApp, createRef<Menu>()], [CdmEntityType.MrsRouterGroup, createRef<Menu>()], [CdmEntityType.MrsRouter, createRef<Menu>()], [CdmEntityType.MrsUser, createRef<Menu>()], [CdmEntityType.MrsDbObject, createRef<Menu>()], ]); private odmTypeToMenuRefMap = new Map<OdmEntityType, RefObject<Menu>>([ [OdmEntityType.ConnectionPage, createRef<Menu>()], [OdmEntityType.Overview, createRef<Menu>()], [OdmEntityType.Notebook, createRef<Menu>()], [OdmEntityType.Script, createRef<Menu>()], [OdmEntityType.AdminPage, createRef<Menu>()], [OdmEntityType.ShellSession, createRef<Menu>()], ]); private ociTypeToMenuRefMap = new Map<OciDmEntityType, RefObject<Menu>>([ [OciDmEntityType.ConfigurationProfile, createRef<Menu>()], [OciDmEntityType.Compartment, createRef<Menu>()], [OciDmEntityType.Bastion, createRef<Menu>()], [OciDmEntityType.ComputeInstance, createRef<Menu>()], [OciDmEntityType.DbSystem, createRef<Menu>()], [OciDmEntityType.HeatWaveCluster, createRef<Menu>()], [OciDmEntityType.LoadBalancer, createRef<Menu>()], ]); private documentTableRef = createRef<TreeGrid>(); private connectionTableRef = createRef<TreeGrid>(); private ociTableRef = createRef<TreeGrid>(); private connectionSectionRef = createRef<AccordionSection>(); private ociSectionRef = createRef<AccordionSection>(); // > 0 if a data model refresh is running. In this case ignore all incoming data model changes // (we are the source of them in this case). private refreshRunning = 0; public constructor(props: IDocumentSideBarProperties) { super(props); this.state = { treeItems: { openDocumentTreeItems: [], connectionTreeItems: [], ociTreeItems: [], }, defaultOpening: false, }; this.addHandledProperties("selectedEntry", "markedSchema", "savedSectionState", "onSelectConnectionItem", "onSelectDocumentItem", "onSelectScriptItem", "onChangeItem", "onSaveState", "onConnectionTreeCommand", "onDocumentTreeCommand", "onScriptTreeCommand", "onOciTreeCommand"); } public override componentDidMount(): void { requisitions.register("refreshConnection", this.refreshConnection); // Create the initial tree items. const context = this.context as DocumentContextType; if (context) { context.connectionsDataModel.subscribe(this.connectionDataModelChanged); context.documentDataModel.subscribe(this.documentDataModelChanged); this.updateTreesFromContext(); } } public override componentWillUnmount(): void { const context = this.context as DocumentContextType; if (context) { context.connectionsDataModel.unsubscribe(this.connectionDataModelChanged); context.documentDataModel.unsubscribe(this.documentDataModelChanged); } requisitions.unregister("refreshConnection", this.refreshConnection); } public override componentDidUpdate(prevProps: IDocumentSideBarProperties): void { if (webSession.runMode === RunMode.SingleServer) { const { defaultOpening } = this.state; const context = this.context as DocumentContextType; if (!defaultOpening && context) { this.setState({ defaultOpening: true }); const firstConnection = context.connectionsDataModel.connections[0]; if (!context.documentDataModel.isOpen(firstConnection.details)) { // Open the first connection and wait for it to finish that. void firstConnection.refresh?.().then(() => { setTimeout(() => { const row = this.connectionTableRef.current?.getRows()[0]; if (row) { row.treeExpand(); } }, 500); this.openNewNotebook(firstConnection); }); // Do not update the trees yet. Instead wait for the new notebook to be opened. return; } } } const { markedSchema, selectedOpenDocument } = this.props; // Check for data model changes and update our trees. this.updateTreesFromContext(); if (this.connectionTableRef.current) { if (markedSchema !== prevProps.markedSchema) { let rows = this.connectionTableRef.current.searchAllRows("caption", markedSchema); rows.forEach((row) => { row.reformat(); }); rows = this.connectionTableRef.current.searchAllRows("caption", prevProps.markedSchema); rows.forEach((row) => { row.reformat(); }); } } if (prevProps.selectedOpenDocument !== selectedOpenDocument) { const { treeItems } = this.state; this.documentTableRef.current?.deselectRow(); if (selectedOpenDocument && selectedOpenDocument !== this.props.overviewId) { const rows = this.documentTableRef.current?.searchAllRows("id", selectedOpenDocument); rows?.forEach((row) => { row.select(); }); } else if (treeItems.openDocumentTreeItems.length > 0) { // No selection or the overview was given, so select the overview (if visible at all). const item = treeItems.openDocumentTreeItems[0]; const rows = this.documentTableRef.current?.searchAllRows("id", item.id); rows?.forEach((row) => { row.select(); }); } } } public render(): ComponentChild { const { savedSectionState } = this.props; const { editing } = this.state; const context = this.context as DocumentContextType; if (!context) { return null; } const documentSectionState = savedSectionState?.get("documentSection") ?? {}; const connectionSectionState = savedSectionState?.get("connectionSection") ?? {}; const ociSectionState = savedSectionState?.get("ociSection") ?? {}; //const shellTaskSectionState = savedSectionState?.get("shellTasksSection") ?? {}; const title = appParameters.embedded ? "MYSQL SHELL GUI" : "MYSQL SHELL WORKBENCH"; const accordionSections: IAccordionSection[] = [{ id: "documentSection", caption: "OPEN EDITORS", stretch: false, minSize: 70, maxSize: 400, resizable: true, expanded: documentSectionState.expanded, initialSize: documentSectionState.size, dimmed: editing != null, actions: [], content: this.renderDocumentsTree(context.documentDataModel.roots), }]; if (webSession.runMode !== RunMode.SingleServer) { accordionSections[0].actions!.push({ icon: Codicon.NewFile, command: { command: "addConsole", tooltip: "Add new console", title: "Add new console", }, }); } const connectionSection: IAccordionSection = { ref: this.connectionSectionRef, id: "connectionSection", caption: "DATABASE CONNECTIONS", stretch: true, expanded: connectionSectionState.expanded, initialSize: connectionSectionState.size ?? 500, resizable: true, minSize: 100, actions: [], content: this.renderConnectionsTree(context.connectionsDataModel.connections), }; accordionSections.push(connectionSection); if (webSession.runMode === RunMode.SingleServer) { connectionSection.actions!.push({ icon: Codicon.CollapseAll, command: { command: "msg.collapseAll", tooltip: "Collapse Connection Node", title: "Collapse Connection Node", }, }); } else { connectionSection.actions!.push({ icon: Codicon.Add, command: { command: "msg.addConnection", tooltip: "Create New DB Connection", title: "Create New DB Connection", }, }, { icon: Codicon.Refresh, command: { command: "msg.refreshConnections", tooltip: "Refresh the Connection List", title: "Refresh the Connection List", }, }, { icon: Codicon.CollapseAll, command: { command: "msg.collapseAll", tooltip: "Collapse All", title: "Collapse All", }, }); } if (webSession.runMode !== RunMode.SingleServer) { accordionSections.push({ ref: this.ociSectionRef, id: "ociSection", caption: "ORACLE CLOUD INFRASTRUCTURE", stretch: true, expanded: ociSectionState.expanded, initialSize: ociSectionState.size, resizable: true, minSize: 100, content: this.renderOciTree(context.ociDataModel.profiles), actions: [{ icon: Codicon.Gear, command: { command: "msg.mds.configureOciProfiles", tooltip: "Configure the OCI Profile list", title: "Configure the OCI Profile list", }, }, { icon: Codicon.Refresh, command: { command: "msg.mds.refreshOciProfiles", tooltip: "Reload the OCI Profile list", title: "Reload the OCI Profile list", }, }], }); } return ( <> <Accordion id="documentSideBar" caption={title} actions={[{ icon: Codicon.KebabHorizontal, tooltip: "More Actions", choices: [{ icon: Codicon.Account, command: { command: "msg.logOut", title: "Log Out", }, }, { icon: Codicon.Bug, command: { command: "msg.fileBugReport", title: "File Bug Report", }, }], }]} sections={accordionSections} onSectionAction={this.handleSectionAction} onSectionExpand={this.handleSectionExpand} onSectionResize={this.handleSectionResize} {...this.unhandledProperties} /> {this.renderConnectionTreeContextMenus()} {this.renderMrsTreeContextMenus()} {this.renderDocumentTreeContextMenus()} {this.renderOciTreeContextMenus()} </> ); } private renderConnectionTreeContextMenus(): ComponentChild { const optionalMenuItems: ComponentChild[] = []; if (webSession.runMode !== RunMode.SingleServer) { optionalMenuItems.push( <MenuItem command={{ title: "Edit DB Connection", command: "msg.editConnection" }} />, <MenuItem command={{ title: "Duplicate this DB Connection", command: "msg.duplicateConnection" }} />, <MenuItem command={{ title: "Delete DB Connection...", command: "msg.removeConnection" }} />, <MenuItem command={{ title: "-", command: "" }} disabled />, <MenuItem id="showSystemSchemas" command={{ title: "Show MySQL System Schemas", command: "msg.showSystemSchemasOnConnection", }} altCommand={{ title: "Hide MySQL System Schemas", command: "msg.hideSystemSchemasOnConnection", }} />, <MenuItem command={{ title: "-", command: "" }} disabled />, <MenuItem command={{ title: "Load SQL Script from Disk...", command: "msg.loadScriptFromDisk" }} />, <MenuItem command={{ title: "Load Dump from Disk...", command: "msg.loadDumpFromDisk" }} disabled />, <MenuItem command={{ title: "-", command: "" }} disabled />, <MenuItem command={{ title: "Open New MySQL Shell Console for this Connection", command: "msg.newSessionUsingConnection", }} />, <MenuItem command={{ title: "-", command: "" }} disabled />, ); } return ( <> <Menu id="connectionContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.Connection)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} isItemDisabled={this.isConnectionMenuItemDisabled} > <MenuItem command={{ title: "Open New Database Connection", command: "msg.openConnection" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> {optionalMenuItems} <MenuItem command={{ title: "Browse the MySQL REST Service Documentation", command: "msg.mrs.docs" }} /> <MenuItem command={{ title: "Configure Instance for MySQL REST Service Support", command: "msg.mrs.configureMySQLRestService", }} /> </Menu> <Menu id="schemaContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.Schema)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Set As Current Database Schema", command: "setCurrentSchemaMenuItem" }} /> <MenuItem command={{ title: "Filter to this Schema", command: "filterMenuItem" }} disabled /> <MenuItem command={{ title: "Show Schema Inspector", command: "inspectorMenuItem" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Dump Schema To Disk..", command: "msg.dumpSchemaToDisk" }} disabled /> <MenuItem command={{ title: "Dump Schema to Disk for MySQL Database Service...", command: "msg.dumpSchemaToDiskForMds", }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Load Data to HeatWave Cluster...", command: "msg.mds.loadToHeatWave" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Add Schema to REST Service...", command: "msg.mrs.addSchema" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Create Schema...", command: "msg.createSchema" }} disabled /> <MenuItem command={{ title: "Alter Schema...", command: "msg.alterSchema" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Drop Schema...", command: "msg.dropSchema" }} disabled /> </Menu > <Menu id="tableContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.Table)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Select Rows", command: "msg.selectRows" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Add Database Object to REST Service...", command: "msg.mrs.addDbObject" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Create Table...", command: "msg.createTable" }} disabled /> <MenuItem command={{ title: "Alter Table...", command: "msg.alterTable" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Drop Table...", command: "msg.dropTable" }} disabled /> <MenuItem command={{ title: "Truncate Table...", command: "msg.truncateTable" }} disabled /> </Menu > <Menu id="columnContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.Column)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Select Rows", command: "msg.selectRows" }} /> <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} /> <MenuItem command={{ title: "Insert Statement", command: "clipboardInsertStatementMenuItem" }} disabled /> <MenuItem command={{ title: "Update Statement", command: "clipboardUpdateStatementMenuItem" }} disabled /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} disabled /> <MenuItem command={{ title: "Insert Statement", command: "editorInsertStatementMenuItem" }} disabled /> <MenuItem command={{ title: "Update Statement", command: "editorUpdateStatementMenuItem" }} disabled /> </MenuItem > </Menu > <Menu id="viewContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.View)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Select Rows", command: "msg.selectRows" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Add Database Object to REST Service...", command: "msg.mrs.addDbObject" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Create View ...", command: "msg.createProcedure" }} disabled /> <MenuItem command={{ title: "Alter View ...", command: "alterViewMenuItem" }} disabled /> <MenuItem command={{ title: "Drop View ...", command: "dropViewMenuItem" }} disabled /> </Menu > <Menu id="eventContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.Event)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Create Event ...", command: "msg.createEvent" }} disabled /> <MenuItem command={{ title: "Alter Event ...", command: "msg.alterEvent" }} disabled /> <MenuItem command={{ title: "Drop Event ...", command: "msg.dropEvent" }} disabled /> </Menu > <Menu id="procedureContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.StoredProcedure)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} disabled /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Add Database Object to REST Service...", command: "msg.mrs.addDbObject" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Create Procedure ...", command: "msg.createProcedure" }} disabled /> <MenuItem command={{ title: "Alter Procedure ...", command: "alterViewMenuItem" }} disabled /> <MenuItem command={{ title: "Drop Procedure ...", command: "dropViewMenuItem" }} disabled /> </Menu > <Menu id="functionContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.StoredFunction)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} disabled /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Add Database Object to REST Service...", command: "msg.mrs.addDbObject" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Create Function ...", command: "msg.createFunction" }} disabled /> <MenuItem command={{ title: "Alter Function ...", command: "msg.alterFunction" }} disabled /> <MenuItem command={{ title: "Drop Function ...", command: "msg.dropFunction" }} disabled /> </Menu > <Menu id="indexContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.Index)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > </Menu > <Menu id="triggerContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.Trigger)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > </Menu> <Menu id="fkContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.ForeignKey)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleConnectionTreeContextMenuItemClick} > <MenuItem command={{ title: "Copy to Clipboard", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToClipboard" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToClipboard" }} /> </MenuItem> <MenuItem command={{ title: "Send to SQL Editor", command: "" }} > <MenuItem command={{ title: "Name", command: "msg.copyNameToEditor" }} /> <MenuItem command={{ title: "Create Statement", command: "msg.copyCreateStatementToEditor" }} /> </MenuItem > </Menu> </> ); } private renderMrsTreeContextMenus(): ComponentChild { return ( <> <Menu id="mrsRootContextMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsRoot)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Configure MySQL REST Service", command: "msg.mrs.configureMySQLRestService", }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Add REST Service...", command: "msg.mrs.addService" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Enable MySQL REST Service", command: "msg.mrs.enableMySQLRestService" }} /> <MenuItem command={{ title: "Disable MySQL REST Service", command: "msg.mrs.disableMySQLRestService" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Show Private Items", command: "msg.mrs.showPrivateItems" }} altCommand={{ title: "Hide Private Items", command: "msg.mrs.hidePrivateItems" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Bootstrap Local MySQL Router Instance", command: "msg.mrs.bootstrapLocalRouter", }} disabled /> <MenuItem command={{ title: "Start Local MySQL Router Instance", command: "msg.mrs.startLocalRouter" }} disabled /> <MenuItem command={{ title: "Stop Local MySQL Router Instance", command: "msg.mrs.stopLocalRouter" }} disabled /> <MenuItem command={{ title: "Kill Local MySQL Router Instances", command: "msg.mrs.killLocalRouters" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Browse the MySQL REST Service Documentation", command: "msg.mrs.docs" }} /> </Menu> <Menu id="mrsServiceMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsService)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Edit REST Service...", command: "msg.mrs.editService" }} /> <MenuItem command={{ title: "Set as Current REST Service", command: "msg.mrs.setCurrentService" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Load from Disk", command: "" }} > <MenuItem command={{ title: "REST Schema From JSON File...", command: "msg.mrs.loadSchemaFromJSONFile", }} disabled /> </MenuItem> <MenuItem command={{ title: "Dump to Disk", command: "" }}> <MenuItem command={{ title: "REST Client SDK Files...", command: "msg.mrs.exportServiceSdk" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> </MenuItem> <MenuItem command={{ title: "Copy to Clipboard", command: "" }}> <MenuItem command={{ title: "Copy CREATE REST SERVICE Statement", command: "msg.mrs.copyCreateServiceSql", }} /> <MenuItem command={{ title: "Copy CREATE REST SERVICE Statement Including Database Objects", command: "msg.mrs.copyCreateServiceSqlIncludeDatabaseEndpoints", }} /> </MenuItem> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Add and Link REST Authentication App...", command: "msg.mrs.addAuthApp", }} /> <MenuItem command={{ title: "Link REST Authentication App...", command: "msg.mrs.linkAuthApp" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete REST Service...", command: "msg.mrs.deleteService" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "MRS Service Documentation", command: "msg.mrs.docs.service" }} /> </Menu> <Menu id="mrsRouterMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsRouter)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Delete Router...", command: "msg.mrs.deleteRouter" }} /> </Menu> <Menu id="mrsSchemaMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsSchema)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Edit REST Schema...", command: "msg.mrs.editSchema" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Load from Disk", command: "" }} > <MenuItem command={{ title: "REST Object From JSON File...", command: "msg.mrs.loadObjectFromJSONFile", }} disabled /> </MenuItem> <MenuItem command={{ title: "Dump to Disk", command: "" }}> <MenuItem command={{ title: "Dump REST Schema To JSON File...", command: "msg.mrs.dumpSchemaToJSONFile", }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Dump CREATE REST SCHEMA Statement...", command: "msg.mrs.dumpCreateSchemaSql", }} disabled /> <MenuItem command={{ title: "Dump CREATE REST SCHEMA Statement Including Database Objects...", command: "msg.mrs.dumpCreateSchemaSqlIncludeDatabaseEndpoints", }} disabled /> </MenuItem> <MenuItem command={{ title: "Copy to Clipboard", command: "" }}> <MenuItem command={{ title: "Copy CREATE REST SCHEMA Statement", command: "msg.mrs.copyCreateSchemaSql", }} /> <MenuItem command={{ title: "Copy CREATE REST SCHEMA Statement Including Database Objects", command: "msg.mrs.copyCreateSchemaSqlIncludeDatabaseEndpoints", }} /> </MenuItem> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete REST Schema...", command: "msg.mrs.deleteSchema" }} /> </Menu> <Menu id="mrsAuthAppGroupMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsAuthAppGroup)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Add New Authentication App", command: "msg.mrs.addAuthApp" }} /> </Menu> <Menu id="mrsAuthAppMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsAuthApp)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Edit REST Authentication App...", command: "msg.mrs.editAuthApp" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Link REST Authentication App to REST Service...", command: "msg.mrs.linkToService", }} /> <MenuItem command={{ title: "Add REST User...", command: "msg.mrs.addUser" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete REST Authentication App...", command: "msg.mrs.deleteAuthApp", }} /> </Menu> <Menu id="mrsServiceAuthAppMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsServiceAuthApp)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Unlink REST Authentication App...", command: "msg.mrs.unlinkAuthApp", }} /> </Menu> <Menu id="mrsUserMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsUser)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Edit User...", command: "msg.mrs.editUser" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete User...", command: "msg.mrs.deleteUser" }} /> </Menu> <Menu id="mrsDbObjectMenu" ref={this.cdmTypeToMenuRefMap.get(CdmEntityType.MrsDbObject)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleMrsContextMenuItemClick} > <MenuItem command={{ title: "Edit REST Object...", command: "msg.mrs.editDbObject" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Open REST Object Request Path in Web Browser", command: "msg.mrs.openDbObjectRequestPath", }} /> <MenuItem command={{ title: "Dump to Disk", command: "" }}> <MenuItem command={{ title: "Export REST Object To JSON File...", command: "msg.mrs.dumpObjectToJSONFile", }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Export CREATE REST OBJECT Statement...", command: "msg.mrs.exportCreateDbObjectSql", }} disabled /> </MenuItem> <MenuItem command={{ title: "Copy to Clipboard", command: "" }}> <MenuItem command={{ title: "Copy REST Object Request Path", command: "msg.mrs.copyDbObjectRequestPath", }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Copy CREATE REST Object Statement", command: "msg.mrs.copyCreateDbObjectSql", }} /> </MenuItem> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete REST Object...", command: "msg.mrs.deleteDbObject" }} /> </Menu> </> ); } private renderDocumentTreeContextMenus(): ComponentChild { return ( <> <Menu id="pageContextMenu" ref={this.odmTypeToMenuRefMap.get(OdmEntityType.ConnectionPage)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleDocumentTreeContextMenuItemClick} customCommand={this.handleDocumentTreeContextMenuCustomCommand} isItemDisabled={this.isDocumentMenuItemDisabled} > <MenuItem command={{ title: "New SQL Script", command: "msg.addScript" }} /> <MenuItem command={{ title: "New JavaScript Script", command: "msg.newScriptJs" }} /> <MenuItem command={{ title: "New TypeScript Script", command: "msg.newScriptTs" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Load SQL Script from Disk", command: "msg.loadScriptFromDisk" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Open New MySQL Shell Console for this Connection", command: "msg.newSessionUsingConnection", }} /> </Menu > </> ); } private renderOciTreeContextMenus(): ComponentChild { return ( <> <Menu id="ociProfileMenu" ref={this.ociTypeToMenuRefMap.get(OciDmEntityType.ConfigurationProfile)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleOciContextMenuItemClick} > <MenuItem command={{ title: "View Config Profile Information", command: "msg.mds.getProfileInfo", }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Set as New Default Config Profile", command: "msg.mds.setDefaultProfile", }} /> </Menu> <Menu id="ociCompartmentMenu" ref={this.ociTypeToMenuRefMap.get(OciDmEntityType.Compartment)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleOciContextMenuItemClick} > <MenuItem command={{ title: "View Compartment Information", command: "msg.mds.getCompartmentInfo", }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Set as Current Compartment", command: "msg.mds.setCurrentCompartment", }} /> </Menu> <Menu id="ociDbSystemMenu" ref={this.ociTypeToMenuRefMap.get(OciDmEntityType.DbSystem)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleOciContextMenuItemClick} > <MenuItem command={{ title: "View DB System Information", command: "msg.mds.getDbSystemInfo", }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Create Connection with Bastion Service", command: "msg.mds.createConnectionViaBastionService", }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Start the DB System", command: "msg.mds.startDbSystem" }} disabled /> <MenuItem command={{ title: "Restart the DB System", command: "msg.mds.restartDbSystem" }} disabled /> <MenuItem command={{ title: "Stop the DB System", command: "msg.mds.stopDbSystem" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete the DB System", command: "msg.mds.deleteDbSystem" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Create MySQL Router Endpoint on new Compute Instance", command: "msg.mds.createRouterEndpoint", }} disabled /> </Menu> <Menu id="ociComputeInstanceMenu" ref={this.ociTypeToMenuRefMap.get(OciDmEntityType.ComputeInstance)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleOciContextMenuItemClick} > <MenuItem command={{ title: "View Compute Instance Information", command: "msg.mds.getComputeInstance", }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Open SSH Bastion Session", command: "msg.mds.openBastionSshSession" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete Compute Instance", command: "msg.mds.deleteComputeInstance" }} disabled /> </Menu> <Menu id="HeatWaveClusterMenu" ref={this.ociTypeToMenuRefMap.get(OciDmEntityType.HeatWaveCluster)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleOciContextMenuItemClick} > <MenuItem command={{ title: "Start the HeatWave Cluster", command: "msg.mds.startHWCluster" }} disabled /> <MenuItem command={{ title: "Stop the HeatWave Cluster", command: "msg.mds.stopHWCluster" }} disabled /> <MenuItem command={{ title: "Restart the HeatWave Cluster", command: "msg.mds.restartHWCluster" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Rescale the HeatWave Cluster", command: "msg.mds.rescaleHWCluster" }} disabled /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete the HeatWave Cluster", command: "msg.mds.deleteHWCluster" }} disabled /> </Menu> <Menu id="ociBastionMenu" ref={this.ociTypeToMenuRefMap.get(OciDmEntityType.Bastion)} placement={ComponentPlacement.BottomLeft} onItemClick={this.handleOciContextMenuItemClick} > <MenuItem command={{ title: "Get Bastion Information", command: "msg.mds.getBastion", }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Set as Current Bastion", command: "msg.mds.setCurrentBastion" }} /> <MenuItem command={{ title: "-", command: "" }} disabled /> <MenuItem command={{ title: "Delete Bastion", command: "msg.mds.deleteBastion" }} disabled /> <MenuItem command={{ title: "Refresh When Bastion Reaches Active State", command: "msg.mds.refreshOnBastionActiveState", }} disabled /> </Menu> </> ); } private showConnectionTreeContextMenu = (rowData: IConnectionTreeItem, e: MouseEvent): boolean => { const targetRect = new DOMRect(e.clientX, e.clientY, 2, 2); this.cdmTypeToMenuRefMap.get(rowData.dataModelEntry.type)?.current?.open(targetRect, false, {}, rowData); return true; }; private showOpenDocumentTreeContextMenu = (rowData: IDocumentTreeItem, e: MouseEvent): boolean => { const targetRect = new DOMRect(e.clientX, e.clientY, 2, 2); this.odmTypeToMenuRefMap.get(rowData.dataModelEntry.type)?.current?.open(targetRect, false, {}, rowData); return true; }; private showOciTreeContextMenu = (rowData: IOciTreeItem, e: MouseEvent): boolean => { const targetRect = new DOMRect(e.clientX, e.clientY, 2, 2); this.ociTypeToMenuRefMap.get(rowData.dataModelEntry.type)?.current?.open(targetRect, false, {}, rowData); return true; }; /** * Determines if the given menu item should be disabled. * * @param item The item to check. * @param payload The current payload of the containing menu. * * @returns `true` if the item should be disabled, `false` otherwise. */ private isConnectionMenuItemDisabled = (item: IMenuItemProperties, payload: unknown): boolean => { if (payload && item.id === "showSystemSchemas") { const entry = payload as IConnectionTreeItem; return entry.dataModelEntry.connection.details.dbType !== DBType.MySQL; } return item.disabled ?? false; }; private connectionTreeCellFormatter = (cell: CellComponent): string | HTMLElement => { const data = cell.getData() as IConnectionTreeItem; let iconName = cdmTypeToEntryIcon.get(data.dataModelEntry.type) ?? Assets.file.defaultIcon; const overlays: IIconOverlay[] = []; let dimEntry = false; switch (data.dataModelEntry.type) { case CdmEntityType.Connection: { if (data.dataModelEntry.details.dbType === DBType.MySQL) { iconName = Assets.db.mysqlConnectionIcon; } else if (data.dataModelEntry.details.dbType === DBType.Sqlite) { iconName = Assets.db.sqliteConnectionIcon; } break; } case CdmEntityType.Schema: { const { markedSchema } = this.props; const isMysql = data.dataModelEntry.connection.details.dbType === DBType.MySQL; if (data.dataModelEntry.caption === markedSchema) { if (isMysql) { iconName = Assets.db.mysqlSchemaCurrentIcon; } else { iconName = Assets.db.sqliteSchemaCurrentIcon; } } else { if (isMysql) { iconName = Assets.db.mysqlSchemaIcon; } else { iconName = Assets.db.sqliteSchemaIcon; } } break; } case CdmEntityType.TableGroup: case CdmEntityType.SchemaGroup: { iconName = cdmSubTypeToDbObjectIcon.get(data.dataModelEntry.subType) ?? Assets.file.defaultIcon; break; } case CdmEntityType.Column: { if (data.dataModelEntry.inPK) { iconName = Assets.db.columnPkIcon; } else if (data.dataModelEntry.nullable) { iconName = Assets.db.columnNullableIcon; } else { iconName = Assets.db.columnNotNullIcon; } break; } case CdmEntityType.AdminPage: { iconName = pageTypeToDocumentIcon.get(data.dataModelEntry.pageType) ?? Assets.file.defaultIcon; break; } case CdmEntityType.MrsRoot: { if (!data.dataModelEntry.serviceEnabled) { overlays.push({ icon: Assets.overlay.statusDotRed, mask: Assets.overlay.statusDotMask }); } else if (data.dataModelEntry.showUpdateAvailable) { overlays.push({ icon: Assets.overlay.updateAvailable, mask: Assets.overlay.updateAvailableMask }); } break; } case CdmEntityType.MrsDbObject: { const type = data.dataModelEntry.details.objectType; iconName = mrsDbObjectTypeToIcon.get(type) ?? Assets.file.defaultIcon; if (data.dataModelEntry.details.enabled === 0) { overlays.push({ icon: Assets.overlay.statusDotRed, mask: Assets.overlay.statusDotMask }); } else if (data.dataModelEntry.details.enabled === EnabledState.PrivateOnly) { dimEntry = true; overlays.push({ icon: Assets.overlay.private, mask: Assets.overlay.statusDotMask }); } else if (data.dataModelEntry.details.requiresAuth === 1) { overlays.push({ icon: Assets.overlay.lock, mask: Assets.overlay.lockMask }); } break; } case CdmEntityType.MrsSchema: case CdmEntityType.MrsContentFile: case CdmEntityType.MrsContentSet: { if (data.dataModelEntry.details.requiresAuth) { overlays.push({ icon: Assets.overlay.lock, mask: Assets.overlay.lockMask }); } else if (data.dataModelEntry.details.enabled === EnabledState.PrivateOnly) { overlays.push({ icon: Assets.overlay.private, mask: Assets.overlay.statusDotMask }); } else if (data.dataModelEntry.details.enabled === EnabledState.Disabled) { overlays.push({ icon: Assets.overlay.statusDotRed, mask: Assets.overlay.statusDotMask }); } if (data.dataModelEntry.details.enabled === EnabledState.PrivateOnly) { // Need a separate check for private items, as we have to show them dimmed regardless // of the overlay they use. dimEntry = true; } break; } case CdmEntityType.MrsService: { iconName = data.dataModelEntry.details.isCurrent ? Assets.mrs.serviceDefaultIcon : Assets.mrs.serviceIcon; if (!data.dataModelEntry.details.enabled) { overlays.push({ icon: Assets.overlay.disabled, mask: Assets.overlay.disabledMask }); } else if (data.dataModelEntry.details.inDevelopment) { overlays.push({ icon: Assets.overlay.inDevelopment, mask: Assets.overlay.inDevelopmentMask }); } else if (data.dataModelEntry.details.published) { overlays.push({ icon: Assets.overlay.live, mask: Assets.overlay.liveMask }); } break; } case CdmEntityType.MrsAuthApp: { if (!data.dataModelEntry.details.enabled) { overlays.push({ icon: Assets.overlay.statusDotRed, mask: Assets.overlay.statusDotMask }); } break; } case CdmEntityType.MrsServiceAuthApp: { overlays.push({ icon: Assets.overlay.link, mask: Assets.overlay.linkMask }); if (!data.dataModelEntry.details.enabled) { overlays.push({ icon: Assets.overlay.statusDotRed, mask: Assets.overlay.statusDotMask }); } break; } case CdmEntityType.MrsRouter: { if (data.dataModelEntry.requiresUpgrade) { overlays.push({ icon: Assets.overlay.statusDotRed, mask: Assets.overlay.statusDotMask }); } else if (!data.dataModelEntry.details.active) { overlays.push({ icon: Assets.overlay.statusDotOrange, mask: Assets.overlay.statusDotMask }); } break; } case CdmEntityType.MrsRouterService: case CdmEntityType.MrsAuthAppService: { overlays.push({ icon: Assets.overlay.link, mask: Assets.overlay.linkMask }); break; } case CdmEntityType.StoredFunction: { // TODO: change icon from standard to the particular language. break; } case CdmEntityType.StoredProcedure: { // TODO: change icon from standard to the particular language. break; } default: } const host = document.createElement("div"); host.className = "connectionTreeEntry sidebarTreeEntry"; let actionBox; let subCaption; if (data.dataModelEntry.description) { subCaption = <Label id="subCaption" caption={data.dataModelEntry.description} />; } switch (data.dataModelEntry.type) { case CdmEntityType.Connection: { const connection = data.dataModelEntry; const runNotebookButton = <Button className="actionButton" data-tooltip="Open New Connection using Notebook" imageOnly onClick={() => { const { onConnectionTreeCommand } = this.props; void onConnectionTreeCommand({ title: "Open New Connection using Notebook", command: "msg.openConnection", arguments: ["notebook"], }, connection); }} > <Icon src={Assets.db.runNotebookIcon} data-tooltip="inherit" /> </Button>; const runScriptButton = <Button className="actionButton" data-tooltip="Open New Connection using SQL Script" imageOnly onClick={() => { const { onConnectionTreeCommand } = this.props; void onConnectionTreeCommand({ title: "Open New Connection using Notebook", command: "msg.openConnection", arguments: ["script"], }, connection); }} > <Icon src={Assets.db.runScriptIcon} data-tooltip="inherit" /> </Button>; const refreshButton = <Button className="actionButton" data-tooltip="Refresh Connection" imageOnly onClick={() => { void this.refreshConnectionTreeEntryChildren(data.dataModelEntry, true, true); }} > <Icon src={Codicon.Refresh} data-tooltip="inherit" /> </Button>; actionBox = <Container className="actionBox" orientation={Orientation.LeftToRight}> {runNotebookButton} {runScriptButton} {refreshButton} </Container>; break; } case CdmEntityType.MrsService: { const { onConnectionTreeCommand } = this.props; const docButton = <Button className="actionButton" data-tooltip="MRS Service Documentation" imageOnly onClick={() => { void onConnectionTreeCommand({ title: "", command: "msg.mrs.docs.service" }, data.dataModelEntry); }} ><Icon src={Assets.misc.docsIcon} data-tooltip="inherit" /></Button>; actionBox = <Container className="actionBox" orientation={Orientation.LeftToRight}> {docButton} </Container>; break; } case CdmEntityType.MrsRouter: { break; } default: } const dimClass = dimEntry ? "dim" : ""; const content = <> <Icon src={iconName} overlays={overlays} className={dimClass} /> <Label id="mainCaption" caption={data.caption} /> {subCaption} {actionBox} </>; render(content, host); return host; }; private openNewNotebook(entry: ConnectionDataModelEntry): void { const connection = entry.connection; void requisitions.execute("openDocument", { connection: connection.details, documentDetails: { id: uuid(), type: OdmEntityType.Notebook, caption: connection.caption, language: "msg", }, }); } private openNewScript(entry: ConnectionDataModelEntry): void { const connection = entry.connection; void requisitions.execute("openDocument", { connection: connection.details, documentDetails: { id: uuid(), type: OdmEntityType.Script, caption: "SQL Script", language: connection.details.dbType === DBType.MySQL ? "mysql" : "sql", }, }); } private documentTreeCellFormatter = (cell: CellComponent): string | HTMLElement => { const data = cell.getData() as IDocumentTreeItem; let actionBox; let iconName: string | Codicon = odmTypeToDocumentIcon.get(data.dataModelEntry.type) ?? Assets.file.defaultIcon; switch (data.dataModelEntry.type) { case OdmEntityType.ConnectionPage: { const page = data.dataModelEntry; const isMySQL = page.details.dbType === DBType.MySQL; if (isMySQL) { iconName = Assets.db.mysqlConnectionIcon; } else if (page.details.dbType === DBType.Sqlite) { iconName = Assets.db.sqliteConnectionIcon; } const newScriptButton = <Button className="actionButton" data-tooltip={isMySQL ? "New MySQL Script" : "New Sqlite Script"} imageOnly onClick={() => { this.handleDocumentTreeContextMenuItemClick({ command: "msg.addScript", title: "" }, false, data.dataModelEntry); }} > <Icon src={Assets.misc.newScriptIcon} data-tooltip="inherit" /> </Button>; const loadScriptButton = <Button className="actionButton" data-tooltip="Load SQL Script from Disk..." imageOnly onClick={() => { this.handleDocumentTreeContextMenuItemClick({ command: "msg.loadScriptFromDisk", title: "" }, false, data.dataModelEntry); }} > <Icon src={Assets.toolbar.loadScriptIcon} data-tooltip="inherit" /> </Button>; actionBox = <Container className="actionBox" orientation={Orientation.LeftToRight}> {newScriptButton} {loadScriptButton} </Container>; break; } case OdmEntityType.AdminPage: { const pageEntry = data.dataModelEntry; iconName = pageTypeToDocumentIcon.get(pageEntry.pageType) ?? Assets.file.defaultIcon; const closeButton = <Button className="actionButton" data-tooltip="Close Document" imageOnly onClick={() => { void requisitions.execute("closeDocument", { connectionId: pageEntry.parent!.details.id, documentId: data.dataModelEntry.id, pageId: pageEntry.id, }); }} > <Icon src={Codicon.Close} data-tooltip="inherit" /> </Button>; actionBox = <Container className="actionBox" orientation={Orientation.LeftToRight}> {closeButton} </Container>; break; } case OdmEntityType.Script: { const pageEntry = data.dataModelEntry; const language = pageEntry.language; iconName = documentTypeToFileIcon.get(language) ?? iconName; break; } case OdmEntityType.Notebook: { break; } case OdmEntityType.StandaloneDocument: { const document = data.dataModelEntry; if (document.language) { iconName = documentTypeToFileIcon.get(document.language) ?? iconName; } else { iconName = Codicon.Json; } break; } default: } switch (data.dataModelEntry.type) { case OdmEntityType.Notebook: case OdmEntityType.Script: case OdmEntityType.AdminPage: case OdmEntityType.StandaloneDocument: case OdmEntityType.ShellSession: { const pageEntry = data.dataModelEntry; const closeButton = <Button className="actionButton" data-tooltip="Close Document" imageOnly onClick={() => { if (pageEntry.parent && pageEntry.parent.type === OdmEntityType.ConnectionPage) { void requisitions.execute("closeDocument", { connectionId: pageEntry.parent.details.id, documentId: data.dataModelEntry.id, pageId: pageEntry.parent.id, }); } else { void requisitions.execute("closeDocument", { documentId: data.dataModelEntry.id, pageId: pageEntry.parent?.id, }); } }} > <Icon src={Codicon.Close} data-tooltip="inherit" /> </Button>; actionBox = <Container className="actionBox" orientation={Orientation.LeftToRight}> {closeButton} </Container>; break; } default: } const host = document.createElement("div"); host.className = "documentTreeEntry sidebarTreeEntry"; const content = <> {iconName && <Icon src={iconName} />} <Label caption={data.caption} /> {actionBox} </>; render(content, host); return host; }; private handleConnectionTreeRowSelected = (row: RowComponent): void => { const { onSelectConnectionItem } = this.props; const entry = row.getData() as IConnectionTreeItem; void onSelectConnectionItem?.(entry.dataModelEntry); }; private handleConnectionTreeRowExpanded = (row: RowComponent): void => { const entry = row.getData() as IConnectionTreeItem; entry.dataModelEntry.state.expanded = true; if (!entry.dataModelEntry.state.isLeaf && !entry.dataModelEntry.state.expandedOnce) { entry.dataModelEntry.state.expandedOnce = true; void this.refreshConnectionTreeEntryChildren(entry.dataModelEntry, true); } }; private handleConnectionTreeRowCollapsed = (row: RowComponent): void => { const entry = row.getData() as IConnectionTreeItem; entry.dataModelEntry.state.expanded = false; }; private isConnectionTreeRowExpanded = (row: RowComponent): boolean => { const { markedSchema } = this.props; const entry = row.getData() as IConnectionTreeItem; const dataModelEntry = entry.dataModelEntry; if (dataModelEntry.type === CdmEntityType.Schema && entry.qualifiedName.name === markedSchema && !entry.dataModelEntry.state.isLeaf && !entry.dataModelEntry.state.expandedOnce) { setTimeout(() => { row.treeExpand(); }, 100); return true; } return entry.dataModelEntry.state.expanded; }; private handleConnectionTreeRowContext = (event: Event, row: RowComponent): void => { const entry = row.getData() as IConnectionTreeItem; this.showConnectionTreeContextMenu(entry, event as MouseEvent); }; private handleConnectionTreeDoubleClick = (e: Event, cell: CellComponent): void => { const item = cell.getData() as IConnectionTreeItem; void requisitions.execute("connectionItemDefaultAction", item.dataModelEntry); }; private handleDocumentTreeRowSelected = (row: RowComponent): void => { const { onSelectDocumentItem } = this.props; const entry = row.getData() as IDocumentTreeItem; void onSelectDocumentItem?.(entry.dataModelEntry); }; private handleDocumentTreeRowExpanded = (row: RowComponent): void => { const documentEntry = row.getData() as IDocumentTreeItem; if (!documentEntry.dataModelEntry.state.isLeaf) { documentEntry.dataModelEntry.state.expanded = true; if (!documentEntry.dataModelEntry.state.expandedOnce) { documentEntry.dataModelEntry.state.expandedOnce = true; void this.refreshDocumentTreeEntryChildren(documentEntry.dataModelEntry); } } }; private handleDocumentTreeRowCollapsed = (row: RowComponent): void => { const entry = row.getData() as IDocumentTreeItem; entry.dataModelEntry.state.expanded = false; }; private isDocumentTreeRowExpanded = (row: RowComponent): boolean => { const entry = row.getData() as IDocumentTreeItem; // Auto expand document tree items. if (!entry.dataModelEntry.state.isLeaf && !entry.dataModelEntry.state.expandedOnce) { this.handleDocumentTreeRowExpanded(row); // If this row represents a connection page, select its first child (a notebook). if (entry.dataModelEntry.type === OdmEntityType.ConnectionPage) { const firstChild = row.getTreeChildren()[0]; if (firstChild) { const { onSelectDocumentItem } = this.props; const childEntry = firstChild.getData() as IDocumentTreeItem; void onSelectDocumentItem?.(childEntry.dataModelEntry); } } } return entry.dataModelEntry.state.expanded; }; private handleDocumentTreeRowContext = (event: Event, row: RowComponent): void => { const entry = row.getData() as IDocumentTreeItem; this.showOpenDocumentTreeContextMenu(entry, event as MouseEvent); }; private handleOciTreeRowExpanded = (row: RowComponent): void => { const ociEntry = row.getData() as IOciTreeItem; if (ociEntry.dataModelEntry.state.isLeaf) { return; } ociEntry.dataModelEntry.state.expanded = true; if (!ociEntry.dataModelEntry.state.expandedOnce) { ociEntry.dataModelEntry.state.expandedOnce = true; /** * Helper to add a list of DM entries to the given row. * * @param entries The entries to add. * @param hasChildren Whether the entries can have children. */ const addEntries = <T extends OciDataModelEntry>(entries: T[], hasChildren: (entry: T) => boolean) => { const children = entries.map((entry) => { const child: IOciTreeItem = { id: entry.id, dataModelEntry: entry, caption: entry.caption, children: hasChildren(entry) ? [] : undefined, }; return child; }); children.forEach((child) => { row.addTreeChild(child); }); }; // If initializing takes longer that the timer runs, show a progress indicator. const timer = setTimeout(() => { this.ociSectionRef.current!.showProgress = true; }, 200); row.getTable().blockRedraw(); ociEntry.dataModelEntry.refresh?.().then(() => { clearTimeout(timer); this.ociSectionRef.current!.showProgress = false; switch (ociEntry.dataModelEntry.type) { case OciDmEntityType.ConfigurationProfile: { const profile = ociEntry.dataModelEntry; addEntries(profile.compartments, () => { return true; }); break; } case OciDmEntityType.Compartment: { const compartment = ociEntry.dataModelEntry; addEntries(compartment.compartments, () => { return true; }); addEntries(compartment.dbSystems, (entry) => { return entry.cluster !== undefined; }); addEntries(compartment.heatWaveClusters, () => { return true; }); addEntries(compartment.computeInstances, () => { return false; }); addEntries(compartment.bastions, () => { return false; }); addEntries(compartment.loadBalancers, () => { return false; }); break; } case OciDmEntityType.DbSystem: { const dbSystem = ociEntry.dataModelEntry; if (dbSystem.cluster) { addEntries([dbSystem.cluster], () => { return false; }); } break; } default: } // Attention: don't move these calls outside of the promise or they will kick in too early. row.getTable().restoreRedraw(); }).catch((error) => { clearTimeout(timer); this.ociSectionRef.current!.showProgress = false; const message = convertErrorToString(error); void ui.showErrorMessage(message, {}); row.getTable().restoreRedraw(); }); } }; private handleOciTreeRowCollapsed = (row: RowComponent): void => { const entry = row.getData() as IOciTreeItem; entry.dataModelEntry.state.expanded = false; }; private isOciTreeRowExpanded = (row: RowComponent): boolean => { const entry = row.getData() as IOciTreeItem; return entry.dataModelEntry.state.expanded; }; private handleOciTreeRowContext = (event: Event, row: RowComponent): void => { const entry = row.getData() as IOciTreeItem; this.showOciTreeContextMenu(entry, event as MouseEvent); }; private ociTreeCellFormatter = (cell: CellComponent): string | HTMLElement => { const data = cell.getData() as IOciTreeItem; const overlays: IIconOverlay[] = []; let image = ociTypeToEntryIcon.get(data.dataModelEntry.type) ?? Assets.file.defaultIcon; switch (data.dataModelEntry.type) { case OciDmEntityType.ConfigurationProfile: { if (data.dataModelEntry.profileData.isCurrent) { image = Assets.oci.profileCurrentIcon; } else { image = Assets.oci.profileIcon; } break; } case OciDmEntityType.Compartment: { if (data.dataModelEntry.compartmentDetails.isCurrent) { image = Assets.file.folderCurrentIcon; } else { image = Assets.file.folderIcon; } break; } case OciDmEntityType.Bastion: { const summary = data.dataModelEntry.summary; if (summary.isCurrent) { image = Assets.oci.bastionCurrentIcon; } else { image = Assets.oci.bastionIcon; } if (summary.lifecycleState !== BastionLifecycleState.Active) { overlays.push({ icon: Assets.overlay.statusDotOrange, mask: Assets.overlay.statusDotMask }); } break; } case OciDmEntityType.DbSystem: { const details = data.dataModelEntry.details; let overlayImage: string | undefined = Assets.overlay.statusDotOrange; // Assume it's not active. const overlayImageMask = Assets.overlay.statusDotMask; if (data.dataModelEntry.cluster) { image = Assets.oci.dbSystemHWIcon; if (details.lifecycleDetails === DbSystem.LifecycleState.Active) { overlayImage = undefined; // Reset overlay, we have an active node. } else if (details.lifecycleState === DbSystem.LifecycleState.Inactive || details.lifecycleState === DbSystem.LifecycleState.Failed) { overlayImage = Assets.overlay.statusDotRed; } } else { image = Assets.oci.dbSystemIcon; if (details.lifecycleState === DbSystem.LifecycleState.Active) { overlayImage = undefined; } else if (details.lifecycleState === DbSystem.LifecycleState.Inactive || details.lifecycleState === DbSystem.LifecycleState.Failed) { overlayImage = Assets.overlay.statusDotRed; } } if (overlayImage) { overlays.push({ icon: overlayImage, mask: overlayImageMask }); } break; } case OciDmEntityType.HeatWaveCluster: { // Sub item of a cluster DB system node. const details = data.dataModelEntry.parent.details; image = Assets.oci.computeIcon; let overlayImage: string | undefined = Assets.overlay.statusDotOrange; // Assume it's not active. const overlayImageMask = Assets.overlay.statusDotMask; if (details.heatWaveCluster) { // Side note: there's no enum defined for the HW cluster lifecycle state. if (details.heatWaveCluster.lifecycleState === "ACTIVE") { overlayImage = undefined; } else if (details.heatWaveCluster.lifecycleState === "INACTIVE" || details.heatWaveCluster.lifecycleState === "FAILED") { overlayImage = Assets.overlay.statusDotRed; } } if (overlayImage) { overlays.push({ icon: overlayImage, mask: overlayImageMask }); } break; } case OciDmEntityType.LoadBalancer: { image = Assets.oci.loadBalancerIcon; const details = data.dataModelEntry.details; if (details.lifecycleState !== LoadBalancer.LifecycleState.Active) { overlays.push({ icon: Assets.overlay.statusDotOrange, mask: Assets.overlay.statusDotMask }); } break; } default: { break; } } const host = document.createElement("div"); host.className = "ociTreeEntry sidebarTreeEntry"; const content = <> <Icon src={image} overlays={overlays} /> <Label caption={data.caption} /> </>; render(content, host); return host; }; private handleSectionAction = (command?: Command): void => { const { onConnectionTreeCommand } = this.props; switch (command?.command) { case "msg.refreshConnections": { void onConnectionTreeCommand?.(command).then((success) => { if (success) { this.updateTreesFromContext(); } }); break; } case "msg.collapseAll": { this.collapseAllConnectionTreeTopLevelItems(); break; } case "msg.mds.configureOciProfiles": { void ui.showWarningMessage("Not implemented yet.", {}); break; } case "msg.mds.refreshOciProfiles": { const context = this.context as DocumentContextType; void context.ociDataModel.updateProfiles().then(() => { this.updateTreesFromContext(); }); break; } default: { if (command) { void onConnectionTreeCommand?.(command); } } } }; private handleSectionExpand = (props: IAccordionProperties, sectionId: string, expanded: boolean): void => { const { onSaveState, savedSectionState = new Map<string, IDocumentSideBarSectionState>() } = this.props; const sectionState = savedSectionState?.get(sectionId) ?? {}; sectionState.expanded = expanded; savedSectionState.set(sectionId, sectionState); onSaveState?.(savedSectionState); }; private handleSectionResize = (_props: IAccordionProperties, info: ISplitterPaneSizeInfo[]): void => { const { onSaveState, savedSectionState } = this.props; const newMap = savedSectionState ? new Map(savedSectionState) : new Map<string, IDocumentSideBarSectionState>(); info.forEach((value) => { const sectionState = newMap.get(value.id) ?? {}; sectionState.size = value.currentSize; newMap.set(value.id, sectionState); }); onSaveState?.(newMap); }; /** * Called from the context menu of the open document tree or by an action button. * * @param propsOrCommand Either the properties of the menu item that was clicked, or the command to execute. * @param altActive Whether the alt key was active when the menu item was clicked. * @param payload The tree item that was clicked or the data model entry of the document to act on. * * @returns `true` if the command was handled, `false` otherwise. */ private handleDocumentTreeContextMenuItemClick = (propsOrCommand: IMenuItemProperties | Command, altActive: boolean, payload: unknown): boolean => { const { onDocumentTreeCommand } = this.props; let command: Command; let dataModelEntry: OpenDocumentDataModelEntry; if (propsOrCommand.title !== undefined) { command = propsOrCommand as Command; dataModelEntry = payload as OpenDocumentDataModelEntry; } else { const p = propsOrCommand as IMenuItemProperties; command = p.command && altActive && p.altCommand ? p.altCommand : p.command; const data = payload as IDocumentTreeItem; dataModelEntry = data.dataModelEntry; } switch (command.command) { default: { void onDocumentTreeCommand(command, dataModelEntry); } } return true; }; /** * Update the actual caption of certain menu items, depending on the connection type. * * @param props The properties of the menu item to customize the command for. * @param payload The tree item that was clicked. * * @returns The updated command, or `undefined` if the command should not be customized. */ private handleDocumentTreeContextMenuCustomCommand = (props: IMenuItemProperties, payload: unknown): Command | undefined => { const data = payload as IDocumentTreeItem; switch (data?.dataModelEntry?.type) { case OdmEntityType.ConnectionPage: { if (props.command.command === "msg.addScript") { const isMySQL = data.dataModelEntry.details.dbType === DBType.MySQL; return { ...props.command, title: isMySQL ? "New MySQL Script" : "New Sqlite Script", }; } break; } default: } return undefined; }; /** * Determines if the given menu item should be disabled. * * @param props The properties of the menu item to check. * @param payload The current payload of the containing menu. * * @returns `true` if the item should be disabled, `false` otherwise. */ private isDocumentMenuItemDisabled = (props: IMenuItemProperties, payload: unknown): boolean => { if (payload) { if (props.command.command === "msg.newSessionUsingConnection") { const entry = payload as IDocumentTreeItem; if (entry.dataModelEntry.type === OdmEntityType.ConnectionPage) { return entry.dataModelEntry.details.dbType !== DBType.MySQL; } } } return props.disabled ?? false; }; private handleConnectionTreeContextMenuItemClick = (props: IMenuItemProperties, altActive: boolean, payload: unknown): boolean => { const { onConnectionTreeCommand } = this.props; const data = payload as IConnectionTreeItem; const command = altActive && props.altCommand ? props.altCommand : props.command; switch (command.command) { case "msg.mrs.configureMySQLRestService": { void onConnectionTreeCommand?.(command, data.dataModelEntry).then(() => { void this.refreshConnectionTreeEntryChildren(data.dataModelEntry, true); }); break; } case "msg.hideSystemSchemasOnConnection": case "msg.showSystemSchemasOnConnection": { const connection = data.dataModelEntry as ICdmConnectionEntry; connection.state.payload ??= {}; connection.state.payload.showSystemSchemas = command.command === "msg.showSystemSchemasOnConnection"; void this.refreshConnectionTreeEntryChildren(data.dataModelEntry, true); break; } case "msg.dropSchema": { void onConnectionTreeCommand?.(command, data.dataModelEntry).then((result) => { if (result.success) { void this.refreshConnectionParentEntry(data, true); } }); break; } case "msg.mrs.addSchema": { void onConnectionTreeCommand?.(command, data.dataModelEntry).then((result) => { if (result.success) { // Find the MRS service entry to refresh its tree node. const mrsRoot = data.dataModelEntry.connection.mrsEntry; const service = mrsRoot?.services.find((s) => { return s.details.id === result.mrsServiceId; }); if (service) { void this.refreshConnectionTreeEntryChildren(service, true); } else if (mrsRoot) { void this.refreshConnectionTreeEntryChildren(mrsRoot, true); } } }); break; } case "msg.mrs.addDbObject": { void onConnectionTreeCommand?.(command, data.dataModelEntry).then((result) => { if (result.success && result.mrsServiceId && result.mrsSchemaId) { // Find the service and the schema in that, where the new object was added. const mrsRoot = data.dataModelEntry.connection.mrsEntry; const service = mrsRoot?.services.find((s) => { return s.details.id === result.mrsServiceId; }); const schema = service?.schemas.find((s) => { return s.details.id === result.mrsSchemaId; }); if (schema) { void this.refreshConnectionTreeEntryChildren(schema, true); } else if (mrsRoot) { void this.refreshConnectionTreeEntryChildren(mrsRoot, true); } } }); break; } default: { void onConnectionTreeCommand?.(command, data.dataModelEntry, data.qualifiedName); } } return true; }; private handleMrsContextMenuItemClick = async (props: IMenuItemProperties, altActive: boolean, payload: unknown): Promise<boolean> => { const { onConnectionTreeCommand } = this.props; const entry = payload as IConnectionTreeItem; const command = altActive && props.altCommand ? props.altCommand : props.command; const tree = this.connectionTableRef.current; try { switch (command.command) { case "msg.mrs.configureMySQLRestService": { void onConnectionTreeCommand?.(command, entry.dataModelEntry).then(() => { void this.refreshConnectionTreeEntryChildren(entry.dataModelEntry, true); }); break; } case "msg.mrs.addAuthApp": case "msg.mrs.linkAuthApp": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshConnectionTreeEntryChildren(entry.dataModelEntry, true).then(() => { if (command.command === "msg.mrs.addAuthApp" && entry.dataModelEntry.type === CdmEntityType.MrsService) { // If the command was sent from a service, refresh also the list of auth apps. void this.refreshConnectionTreeEntryChildren( entry.dataModelEntry.parent.authAppGroup, true); } }); } }); break; } case "msg.mrs.addService": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { const mrsRoot = entry.dataModelEntry as ICdmRestRootEntry; void this.refreshConnectionTreeEntryChildren(mrsRoot, true).then(() => { void this.refreshConnectionTreeEntryChildren(mrsRoot.routerGroup, true, true); }); } }); break; } case "msg.mrs.enableMySQLRestService": case "msg.mrs.disableMySQLRestService": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshTreeEntry(tree, entry.dataModelEntry, true); } // There's no need to update the routers group, as they only contain published services // which don't show the enabled state. }); break; } case "msg.mrs.showPrivateItems": case "msg.mrs.hidePrivateItems": { const mrsRoot = entry.dataModelEntry as ICdmRestRootEntry; mrsRoot.showPrivateItems = command.command === "msg.mrs.showPrivateItems"; void this.refreshConnectionTreeEntryChildren(entry.dataModelEntry, false, true); break; } case "msg.mrs.editService": { const done = await onConnectionTreeCommand(command, entry.dataModelEntry); if (done) { // Update the entire services list, as we can have a changed default state. const mrsService = entry.dataModelEntry as ICdmRestServiceEntry; await this.refreshTreeEntry(tree, mrsService, true); await this.refreshConnectionTreeEntryChildren(mrsService.parent, true); // Then update also the router nodes, as we may have changed the // publication state of a service. const routers = mrsService.parent.routerGroup.routers; routers.forEach((router) => { void this.refreshConnectionTreeEntryChildren(router, true); }); // And the service entry itself needs an update. await this.refreshConnectionTreeEntryChildren(mrsService, false); } break; } case "msg.mrs.deleteService": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { const service = entry.dataModelEntry as ICdmRestServiceEntry; void this.refreshConnectionTreeEntryChildren(service.parent, true).then(() => { void this.refreshConnectionTreeEntryChildren(service.parent.routerGroup, true, true); }); } }); break; } case "msg.mrs.setCurrentService": case "msg.mrs.deleteSchema": case "msg.mrs.deleteDbObject": case "msg.mrs.editDbObject": case "msg.mrs.deleteUser": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshConnectionParentEntry(entry, true); } }); break; } case "msg.mrs.editSchema": { const mrsSchema = entry.dataModelEntry as ICdmRestSchemaEntry; void onConnectionTreeCommand(command, mrsSchema).then((done) => { if (done) { // If the schema moved from one MRS service to another, // refresh the parent of the old service. if (mrsSchema.details.serviceId !== mrsSchema.parent.details.id) { const mrsRoot = mrsSchema.parent.parent; let service = mrsRoot?.services.find((s) => { return s.details.id === mrsSchema.details.serviceId; }); if (service) { void this.refreshConnectionTreeEntryChildren(service, true); } service = mrsRoot?.services.find((s) => { return s.details.id === mrsSchema.parent.details.id; }); if (service) { void this.refreshConnectionTreeEntryChildren(service, true); } } else { // Otherwise just update the schema entry. void this.refreshTreeEntry(tree, entry.dataModelEntry, true); } } }); break; } case "msg.mrs.editAuthApp": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshTreeEntry(tree, entry.dataModelEntry, true); // Also refresh all MRS services, as the app may have been linked to a service. const authApp = entry.dataModelEntry as ICdmRestAuthAppEntry; void this.refreshConnectionTreeEntryChildren(authApp.parent.parent, true, true); } }); break; } case "msg.mrs.editUser": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshTreeEntry(tree, entry.dataModelEntry, true); } }); break; } case "msg.mrs.deleteAuthApp": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshConnectionParentEntry(entry, true).then(() => { const authApp = entry.dataModelEntry as ICdmRestAuthAppEntry; void this.refreshConnectionTreeEntryChildren(authApp.parent.parent, true, true); }); } }); break; } case "msg.mrs.unlinkAuthApp": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshConnectionParentEntry(entry, true); } }); break; } case "msg.mrs.addUser": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshConnectionTreeEntryChildren(entry.dataModelEntry, true); } }); break; } case "msg.mrs.linkToService": { void onConnectionTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { // Linking to a service means to refresh the service children. At this point we don't // know the service the app was linked to, so refresh all MRS services. const authApp = entry.dataModelEntry as ICdmRestAuthAppEntry; authApp.parent.parent.services.forEach((service) => { void this.refreshConnectionTreeEntryChildren(service, true); }); } }); break; } default: { void onConnectionTreeCommand(command, entry.dataModelEntry); } } } catch (error) { const message = convertErrorToString(error); void ui.showErrorMessage(`Error while running command: ${command.command}. ${message}`, {}); } return true; }; private handleOciContextMenuItemClick = (props: IMenuItemProperties, altActive: boolean, payload: unknown): boolean => { const { onOciTreeCommand } = this.props; const entry = payload as IOciTreeItem; const command = altActive && props.altCommand ? props.altCommand : props.command; try { switch (command.command) { case "msg.mds.setDefaultProfile": { void onOciTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { const context = this.context as DocumentContextType; void context.ociDataModel.updateProfiles().then(() => { this.updateTreesFromContext(); }); } }); break; } case "msg.mds.setCurrentCompartment": { void onOciTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { // Setting a different compartment to current will affect the profile node // under which the compartment is listed. const compartment = entry.dataModelEntry as IOciDmCompartment; // Walk up the compartment chain to find the profile node. let parent = compartment.parent; while (parent && parent.type !== OciDmEntityType.ConfigurationProfile) { parent = parent.parent; } void this.refreshOciTreeEntryChildren(parent); } }); break; } case "msg.mds.setCurrentBastion": { void onOciTreeCommand(command, entry.dataModelEntry).then((done) => { if (done) { void this.refreshOciParentEntry(entry); } }); break; } default: { void onOciTreeCommand(command, entry.dataModelEntry); } } } catch (error) { const message = convertErrorToString(error); void ui.showErrorMessage(`Error while running command: ${command.command}. ${message}`, {}); } return true; }; /** * Triggered when one or all connections need to be refreshed. * * @param connection The connection to refresh. If not provided, all connections are refreshed. * * @returns A promise that resolves to `true`. */ private refreshConnection = async (connection?: ICdmConnectionEntry): Promise<boolean> => { if (connection) { await this.refreshConnectionTreeEntryChildren(connection, true); } else { this.updateTreesFromContext(); } return Promise.resolve(true); }; /** * Creates a list of entries for the connection tree. * * @param connections The connections to create the entries for. * * @returns The list of entries. */ private renderConnectionsTree(connections: ICdmConnectionEntry[]): ComponentChild { const connectionTreeColumns: ColumnDefinition[] = [{ title: "", field: "caption", resizable: false, hozAlign: "left", formatter: this.connectionTreeCellFormatter, cellDblClick: this.handleConnectionTreeDoubleClick, }]; const connectionTreeOptions: ITreeGridOptions = { treeColumn: "caption", selectionType: SelectionType.Single, showHeader: false, layout: "fitColumns", }; const connectionSectionContent = connections.length === 0 ? <Accordion.Item caption="<no connections>" /> : <TreeGrid ref={this.connectionTableRef} options={connectionTreeOptions} columns={connectionTreeColumns} onRowSelected={this.handleConnectionTreeRowSelected} onRowExpanded={this.handleConnectionTreeRowExpanded} onRowCollapsed={this.handleConnectionTreeRowCollapsed} isRowExpanded={this.isConnectionTreeRowExpanded} onRowContext={this.handleConnectionTreeRowContext} />; return connectionSectionContent; } private connectionDataModelChanged = ( list: Readonly<Array<ISubscriberActionType<ConnectionDataModelEntry>>>): void => { if (this.refreshRunning) { return; } const tree = this.connectionTableRef.current; list.forEach((action) => { switch (action.action) { case "add": { if (action.entry) { void this.refreshConnectionTreeEntryChildren(action.entry, false); } break; } case "remove": { if (action.entry?.type === CdmEntityType.Connection) { const context = this.context as DocumentContextType; if (context) { // Remove the connection from the backend. void context.connectionsDataModel.dropItem(action.entry); } requisitions.executeRemote("connectionRemoved", action.entry.details); } // Finally, refresh our UI. const entry = action.entry as ConnectionDataModelEntry; if ("parent" in entry && entry.parent) { void this.refreshConnectionTreeEntryChildren(entry.parent, false); } break; } case "update": { if (action.entry) { switch (action.entry.type) { case CdmEntityType.Connection: { void this.refreshConnectionTreeEntryChildren(action.entry, false); break; } case CdmEntityType.MrsRoot: { void this.refreshConnectionTreeEntryChildren(action.entry, false); break; } default: { void this.refreshTreeEntry(tree, action.entry, false); } } } break; } case "clear": { // TODO: implement this. break; } default: } }); this.forceUpdate(); }; private documentDataModelChanged = ( list: Readonly<Array<ISubscriberActionType<OpenDocumentDataModelEntry>>>): void => { list.forEach((action) => { switch (action.action) { case "add": case "remove": { const entry = action.entry as OpenDocumentDataModelEntry; if (entry.type === OdmEntityType.ShellSession) { this.updateTreesFromContext(); } if ("parent" in entry && entry.parent) { void this.refreshDocumentTreeEntryChildren(entry.parent as OpenDocumentDataModelEntry) .then((result) => { if (!result) { // The tree entry could not be updated because it was not found in the tree. // Schedule another refresh. setTimeout(() => { void this.refreshDocumentTreeEntryChildren( entry.parent as OpenDocumentDataModelEntry); }); } }); } break; } case "update": { if (action.entry) { void this.refreshDocumentTreeEntryChildren(action.entry); } break; } case "clear": { // TODO: implement this. break; } default: } }); }; private renderDocumentsTree = (documents: OpenDocumentDataModelEntry[]): ComponentChild => { const { selectedOpenDocument } = this.props; const documentTreeColumns: ColumnDefinition[] = [{ title: "", field: "caption", resizable: false, hozAlign: "left", formatter: this.documentTreeCellFormatter, }]; const documentTreeOptions: ITreeGridOptions = { treeColumn: "caption", selectionType: SelectionType.Single, showHeader: false, layout: "fitColumns", }; const documentSectionContent = documents.length === 0 ? <Accordion.Item caption="<no documents>" /> : <TreeGrid ref={this.documentTableRef} options={documentTreeOptions} columns={documentTreeColumns} selectedRows={[selectedOpenDocument]} onRowSelected={this.handleDocumentTreeRowSelected} onRowExpanded={this.handleDocumentTreeRowExpanded} onRowCollapsed={this.handleDocumentTreeRowCollapsed} isRowExpanded={this.isDocumentTreeRowExpanded} onRowContext={this.handleDocumentTreeRowContext} />; return documentSectionContent; }; /*private renderShellTasksTree = (): ComponentChild => { return <Accordion.Item caption="<no tasks>" />; };*/ private renderOciTree = (profiles: IOciDmProfile[]): ComponentChild => { if (profiles.length === 0) { return <Accordion.Item caption="<no profiles found>" />; } const ociTreeColumns: ColumnDefinition[] = [{ title: "", field: "caption", resizable: false, hozAlign: "left", formatter: this.ociTreeCellFormatter, }]; const ociTreeOptions: ITreeGridOptions = { treeColumn: "caption", selectionType: SelectionType.Single, showHeader: false, layout: "fitColumns", }; const ociSectionContent = <TreeGrid ref={this.ociTableRef} options={ociTreeOptions} columns={ociTreeColumns} // onRowSelected={this.handleOciTreeRowSelected} onRowExpanded={this.handleOciTreeRowExpanded} onRowCollapsed={this.handleOciTreeRowCollapsed} isRowExpanded={this.isOciTreeRowExpanded} onRowContext={this.handleOciTreeRowContext} />; return ociSectionContent; }; /** * Selectively updates root tree items based on the current data models. Items whose referenced DM entry are not * found in the current data model are removed and the parent element is reset to an unexpanded + uninitialized * state. */ private updateTreesFromContext(): void { const context = this.context as DocumentContextType; if (!context) { return; } const [treeItems, changed] = this.updateRootTreeItems(context); if (changed) { if (this.connectionTableRef.current) { void this.connectionTableRef.current.setData(treeItems.connectionTreeItems, SetDataAction.Replace); } if (this.documentTableRef.current) { void this.documentTableRef.current.setData(treeItems.openDocumentTreeItems, SetDataAction.Replace); } if (this.ociTableRef.current) { void this.ociTableRef.current.setData(treeItems.ociTreeItems, SetDataAction.Replace); } this.setState({ treeItems }); } } private updateRootTreeItems(context: DocumentContextType): [ISideBarTreeItems, boolean] { const { treeItems } = this.state; let changed = false; const openDocumentTreeItems = treeItems.openDocumentTreeItems; const documentRoots = context.documentDataModel.roots; documentRoots.forEach((document, index) => { if (index >= openDocumentTreeItems.length || openDocumentTreeItems[index].dataModelEntry !== document) { changed = true; openDocumentTreeItems[index] = this.generateTreeItem(document); } }); // If there are any additional items in the tree, remove them. if (openDocumentTreeItems.length > documentRoots.length) { changed = true; openDocumentTreeItems.splice(context.documentDataModel.roots.length); } const connectionTreeItems = treeItems.connectionTreeItems; context.connectionsDataModel.connections.forEach((connection, index) => { if (index >= connectionTreeItems.length || connectionTreeItems[index].dataModelEntry !== connection) { changed = true; connectionTreeItems[index] = { id: connection.id, dataModelEntry: connection, caption: connection.caption, qualifiedName: {}, children: [], }; } }); if (connectionTreeItems.length > context.connectionsDataModel.connections.length) { changed = true; connectionTreeItems.splice(context.connectionsDataModel.connections.length); } const ociTreeItems = treeItems.ociTreeItems; context.ociDataModel.profiles.forEach((profile, index) => { if (index >= ociTreeItems.length || ociTreeItems[index].dataModelEntry !== profile) { changed = true; ociTreeItems[index] = { id: profile.id, dataModelEntry: profile, caption: profile.caption, children: [], }; } }); if (ociTreeItems.length > context.ociDataModel.profiles.length) { changed = true; ociTreeItems.splice(context.ociDataModel.profiles.length); } return [ { openDocumentTreeItems, connectionTreeItems, ociTreeItems }, changed, ]; } /** * Refreshes the parent tree entry of the given item, if there's one. Not all entries have a parent entry. * * @param item The item whose parent should be refreshed. * @param needRefresh True if the data model entry should be refreshed before updating the tree entry. */ private async refreshConnectionParentEntry(item: IConnectionTreeItem, needRefresh: boolean): Promise<void> { if ("parent" in item.dataModelEntry) { const parent = item.dataModelEntry.parent; await this.refreshConnectionTreeEntryChildren(parent, needRefresh); } } /** * Refreshes the parent tree entry of the given item, if there's one. Not all entries have a parent entry. * * @param item The item whose parent should be refreshed. */ private async refreshOciParentEntry(item: IOciTreeItem): Promise<void> { if ("parent" in item.dataModelEntry) { const parent = item.dataModelEntry.parent; await this.refreshOciTreeEntryChildren(parent); } } /** * Refreshes the tree entry for the given data model entry by reloading its properties and replacing its caption * with the one from the given entry. * * @param table The tree table which contains the given entry. * @param entry The tree entry to update. * @param needRefresh True if the data model entry should be refreshed before updating the tree entry. */ private async refreshTreeEntry(table: TreeGrid | null, entry: IDataModelBaseEntry, needRefresh: boolean): Promise<void> { const rows = table?.searchAllRows("id", entry.id); if (!table || !rows || rows?.length === 0) { return; } if (needRefresh) { try { ++this.refreshRunning; await entry.refresh?.(); } finally { --this.refreshRunning; } } const row = rows[0]; await row.update({ caption: entry.caption }); row.reformat(); } /** * Updates all child tree items of the row with the given entry in the connection tree. * We use the TreeGrid API to add and remove rows, which is more efficient than re-rendering the entire tree. * These APIs also update our stored tree structure, so we don't need to update it manually. * * @param entry The item to update. * @param needRefresh Whether the data model entry should be refreshed before updating the tree. * @param recursive Whether to update the children recursively. */ private async refreshConnectionTreeEntryChildren(entry: ConnectionDataModelEntry, needRefresh: boolean, recursive: boolean = false): Promise<void> { const table = this.connectionTableRef.current; const rows = table?.searchAllRows("id", entry.id); if (!table || !rows || rows?.length === 0) { return; } const row = rows[0]; const data = row.getData() as IConnectionTreeItem; const schemaName = entry.type === CdmEntityType.Schema ? data.caption : data.qualifiedName.schema; const tableName = entry.type === CdmEntityType.Table ? data.caption : data.qualifiedName.table; // If initializing takes longer that the timer runs, show a progress indicator. const timer = setTimeout(() => { this.connectionSectionRef.current!.showProgress = true; }, 200); if (needRefresh) { try { ++this.refreshRunning; await entry.refresh?.((result?: string | Error) => { if (result instanceof Error) { void ui.showErrorMessage(convertErrorToString(result), {}); } else if (typeof result === "string") { void ui.setStatusBarMessage(result, 15000); } }); clearTimeout(timer); this.connectionSectionRef.current!.showProgress = false; } catch (error) { clearTimeout(timer); this.connectionSectionRef.current!.showProgress = false; void ui.showErrorMessage(convertErrorToString(error), {}); return; } finally { --this.refreshRunning; } } let children = entry.getChildren?.(); try { table.beginUpdate(); let generator = this.generateConnectionTreeChild.bind(this, schemaName, tableName); switch (entry.type) { case CdmEntityType.Connection: { if (data.dataModelEntry.state.expandedOnce) { if (children && !data.dataModelEntry.state.payload?.showSystemSchemas) { // Remove all system schemas from the child list. children = children.filter((schema) => { return !systemSchemas.has(schema.caption); }); } await this.diffTreeEntries(row, generator, recursive, children); } break; } case CdmEntityType.Schema: case CdmEntityType.SchemaGroup: { await this.diffTreeEntries(row, generator, recursive, children); break; } case CdmEntityType.TableGroup: case CdmEntityType.Table: { await this.diffTreeEntries(row, generator, recursive, children); break; } case CdmEntityType.MrsSchema: { generator = this.generateConnectionTreeChild.bind(this, entry.caption, undefined); await this.diffTreeEntries(row, generator, recursive, children); break; } default: { await this.diffTreeEntries(row, generator, recursive, children); } } if (recursive && children) { for (const child of children) { await this.refreshConnectionTreeEntryChildren(child, true, child.state.expandedOnce); } } } finally { table.endUpdate(); clearTimeout(timer); this.connectionSectionRef.current!.showProgress = false; } } /** * Updates all child tree items of the row with the given entry in the open documents tree. * * @param entry The item to update. * * @returns `true` if the tree was updated, `false` otherwise. */ private async refreshDocumentTreeEntryChildren(entry: OpenDocumentDataModelEntry): Promise<boolean> { const { treeItems } = this.state; const table = this.documentTableRef.current; if (!table) { return false; } const rowIndices = this.determineTreeRowIndex(treeItems.openDocumentTreeItems, entry, this.generateTreeItem); const row = table.getRowFromIndex(rowIndices); if (!row) { return false; } try { table.beginUpdate(); switch (entry.type) { case OdmEntityType.ConnectionPage: case OdmEntityType.ShellSessionRoot: { await this.diffTreeEntries(row, this.generateTreeItem, false, entry.getChildren?.()); break; } default: } } catch (error) { const message = convertErrorToString(error); void ui.showErrorMessage(`Failed to refresh tree entries: ${message}`, {}); } finally { table.endUpdate(); } return true; } /** * Updates all child tree items of the row with the given entry in the open documents tree. * * @param entry The item to update. */ private async refreshOciTreeEntryChildren(entry: OciDataModelEntry): Promise<void> { const { treeItems } = this.state; const table = this.ociTableRef.current; if (!table) { return; } const rowIndices = this.determineTreeRowIndex(treeItems.ociTreeItems, entry, this.generateTreeItem); const row = table.getRowFromIndex(rowIndices); if (!row) { return; } try { const generator = this.generateTreeItem; table.beginUpdate(); switch (entry.type) { case OciDmEntityType.ConfigurationProfile: { await entry.refresh?.(); await this.diffTreeEntries(row, generator, false, entry.getChildren?.()); break; } case OciDmEntityType.Bastion: { await this.diffTreeEntries(row, generator, false, entry.getChildren?.()); break; } default: } } catch (error) { const message = convertErrorToString(error); void ui.showErrorMessage(`Failed to refresh tree entries: ${message}`, {}); } finally { table.endUpdate(); } } /** * Takes a list of data model entries and compares them to the child tree entries of the given row. If an entry is * not in the data model, it is removed from the tree. If no tree entry exists for a given model entry, it is added. * * @param row The row to add the children to. * @param generator The function to generate a tree item for a given data model entry. * @param recursive Whether to update the children recursively. * @param entries The entries to compare. */ private async diffTreeEntries<T extends IDataModelBaseEntry>(row: RowComponent, generator: TreeItemGenerator<T>, recursive: boolean, entries?: T[]): Promise<void> { if (!entries) { const data = row.getData() as IBaseTreeItem<T>; data.children = undefined; return; } let existingTreeItems = row.getTreeChildren(); // Remove tree entries that are not in the data model. for (const child of existingTreeItems) { const data = child.getData() as IBaseTreeItem<T>; if (!entries.find((entry) => { return entry === data.dataModelEntry; })) { await child.delete(); } } existingTreeItems = row.getTreeChildren(); const newList: T[] = []; for (const entry of entries) { // Check if the data model entry already exists in the list of tree items. const item = existingTreeItems.find((child) => { const data = child.getData() as IBaseTreeItem<T>; return data.dataModelEntry === entry; }); if (item) { const data = item.getData() as IBaseTreeItem<T>; newList.push(data.dataModelEntry); // Diff children if the entry was expanded once. if (data.dataModelEntry.state.expandedOnce && recursive) { await this.diffTreeEntries(item, generator, true, entry.getChildren?.() as T[]); } } else { newList.push(entry); } } // Replace the list of tree items with the new one. for (const entry of existingTreeItems.reverse()) { await entry.delete(); } for (const entry of newList) { const child = generator(entry); row.addTreeChild(child); } } /** * A specialized version of the generic generateTreeItem method that generates a tree item for a connection data * model entry. * * @param schema The name of the schema the entry belongs to. * @param table The name of the table the entry belongs to. * @param entry The connection data model entry to create the tree item for. * * @returns The tree item for the given data model entry. */ private generateConnectionTreeChild = <T extends IDataModelBaseEntry>(schema: string | undefined, table: string | undefined, entry: T): IConnectionTreeItem => { const item = this.generateTreeItem<IDataModelBaseEntry>(entry) as IConnectionTreeItem; item.qualifiedName = { schema, table, name: entry.caption, }; return item; }; /** * @returns the tree item for the given data model entry. * * @param entry The document entry to create the tree item for. */ private generateTreeItem = <T extends IDataModelBaseEntry>(entry: T): IBaseTreeItem<T> => { // Check if the entry already has the internal tree item member. if ("$treeItem" in entry) { return entry.$treeItem as IBaseTreeItem<T>; } const treeItem: IBaseTreeItem<T> = { id: entry.id, dataModelEntry: entry, caption: entry.caption, children: entry.state.isLeaf ? undefined : [], }; Object.defineProperty(entry, "$treeItem", { value: treeItem, enumerable: false, writable: false, }); return treeItem; }; /** * Determines the row position for the given entry in the given item tree. This is structured as a path * with indices for each level of the tree. * * @param list The list of items to search in. * @param entry The entry to determine the row index for. * @param generator The function to generate a tree item for a given data model entry. * * @returns The row index path for the entry. */ private determineTreeRowIndex<T extends IDataModelBaseEntry>(list: Array<IBaseTreeItem<T>>, entry: T, generator: TreeItemGenerator<T>): number[] { let current: T | undefined = entry; const chain: Array<IBaseTreeItem<T>> = []; while (current) { if ("provider" in current || !("parent" in current)) { // Stop the loop when we reach a provider entry. break; } chain.unshift(generator(current)); current = current.parent as T; } return this.treePathFromItemChain(list, chain); } /** * Creates a list of child indexes for the given chain of items in the given list of items. * The actual type of the data model entry in the tree item doesn't matter here. * Only references are compared. * * @param itemList The list of items to search in. * @param chain The chain of items to find the indexes for. * * @returns The list of indexes for the chain of items. */ private treePathFromItemChain<T extends IBaseTreeItem<unknown>>(itemList: T[], chain: T[]): number[] { let currentChildren: Array<IBaseTreeItem<unknown>> = itemList; const indices: number[] = []; for (const item of chain) { const index = currentChildren.findIndex((child) => { return child.dataModelEntry === item.dataModelEntry; }); if (index === -1) { break; } indices.push(index); currentChildren = (item.children ?? []); } return indices; } private collapseAllConnectionTreeTopLevelItems() { const table = this.connectionTableRef.current; if (!table) { return; } const rows = table.getRows(); rows.forEach((row) => { row.treeCollapse(); }); } }