diff --git a/.eslintignore b/.eslintignore index 02b4d7eb7..2b837830b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -44,7 +44,6 @@ src/Definitions/png.d.ts src/Definitions/svg.d.ts src/Explorer/ComponentRegisterer.test.ts src/Explorer/ComponentRegisterer.ts -src/Explorer/ContextMenuButtonFactory.ts src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts src/Explorer/Controls/DynamicList/DynamicList.test.ts @@ -105,8 +104,6 @@ src/Explorer/Notebook/NotebookContainerClient.ts src/Explorer/Notebook/NotebookContentClient.ts src/Explorer/Notebook/NotebookContentItem.ts src/Explorer/Notebook/NotebookUtil.ts -src/Explorer/OpenActions.test.ts -src/Explorer/OpenActions.ts src/Explorer/OpenActionsStubs.ts src/Explorer/Panes/AddDatabasePane.ts src/Explorer/Panes/AddDatabasePane.test.ts @@ -135,7 +132,6 @@ src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts -src/Explorer/Tables/QueryBuilder/QueryViewModel.ts src/Explorer/Tables/TableDataClient.ts src/Explorer/Tables/TableEntityProcessor.ts src/Explorer/Tables/Utilities.ts @@ -145,14 +141,11 @@ src/Explorer/Tabs/DocumentsTab.test.ts src/Explorer/Tabs/DocumentsTab.ts src/Explorer/Tabs/GraphTab.ts src/Explorer/Tabs/MongoDocumentsTab.ts -src/Explorer/Tabs/MongoQueryTab.ts -src/Explorer/Tabs/MongoShellTab.ts +# src/Explorer/Tabs/MongoQueryTab.ts +# src/Explorer/Tabs/MongoShellTab.ts src/Explorer/Tabs/NotebookV2Tab.ts -src/Explorer/Tabs/QueryTab.test.ts -src/Explorer/Tabs/QueryTab.ts -src/Explorer/Tabs/QueryTablesTab.ts src/Explorer/Tabs/ScriptTabBase.ts -src/Explorer/Tabs/StoredProcedureTab.ts +# src/Explorer/Tabs/StoredProcedureTab.ts src/Explorer/Tabs/TabComponents.ts src/Explorer/Tabs/TabsBase.ts src/Explorer/Tabs/TriggerTab.ts @@ -161,7 +154,6 @@ src/Explorer/Tree/AccessibleVerticalList.ts src/Explorer/Tree/Collection.test.ts src/Explorer/Tree/Collection.ts src/Explorer/Tree/ConflictId.ts -src/Explorer/Tree/Database.ts src/Explorer/Tree/DocumentId.ts src/Explorer/Tree/ObjectId.ts src/Explorer/Tree/ResourceTokenCollection.ts @@ -205,9 +197,6 @@ src/ResourceProvider/IResourceProviderClient.test.ts src/ResourceProvider/IResourceProviderClient.ts src/ResourceProvider/ResourceProviderClient.ts src/ResourceProvider/ResourceProviderClientFactory.ts -src/RouteHandlers/RouteHandler.ts -src/RouteHandlers/TabRouteHandler.test.ts -src/RouteHandlers/TabRouteHandler.ts src/Shared/Constants.ts src/Shared/DefaultExperienceUtility.test.ts src/Shared/DefaultExperienceUtility.ts @@ -263,7 +252,6 @@ src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/EditorNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx -src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx diff --git a/less/documentDB.less b/less/documentDB.less index 159b03cd7..271f53992 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -724,45 +724,24 @@ execute-sproc-params-pane { .results-container, .errors-container { - padding: @MediumSpace 0px 0px @MediumSpace; height: 100%; .flex-display(); .flex-direction(); overflow: hidden; - .toggles { - height: @ToggleHeight; - width: @ToggleWidth; - margin-left: @MediumSpace; - - &:focus { - .focus(); - } - - .tab { - margin-right: @MediumSpace; - } - - .toggleSwitch { - .toggleSwitch(); - } - - .selectedToggle { - .selectedToggle(); - } - - .unselectedToggle { - .unselectedToggle(); - } - } - .enterInputParameters { padding: @LargeSpace @MediumSpace; } + + div[role="tabpanel"] { + height: 100%; + padding-bottom: 50px; + } } .errors-container { padding-left: (2 * @MediumSpace); + padding: @MediumSpace 0px 0px @MediumSpace; .errors-header { font-weight: 700; font-size: @DefaultFontSize; @@ -3088,4 +3067,11 @@ settings-pane { .hiddenMain { display: none; height: 0px; -} \ No newline at end of file +} +.spinner { + width: 100%; + position: absolute; + z-index: 1; + background: white; + height: 100%; +} diff --git a/less/forms.less b/less/forms.less index ba771a108..572134c26 100644 --- a/less/forms.less +++ b/less/forms.less @@ -200,4 +200,12 @@ .migration:disabled { background-color: #ccc; +} + +.trigger-field { + width: 40%; + margin-top: 10px +} +.trigger-form { + padding: 10px 30px 10px 30px; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4c95dc071..997269344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3709,14 +3709,84 @@ } }, "@nteract/editor": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.2.tgz", - "integrity": "sha512-Wtj0kJUSoBZsWUh82JGt6miqYS0jt0k+3SD3cnW9socayxp2KB0Qbqhh2NtrF9ysxVHWnQT8iUarJjpGIdNyng==", + "version": "10.1.12", + "resolved": "https://registry.npmjs.org/@nteract/editor/-/editor-10.1.12.tgz", + "integrity": "sha512-bsUrCctukjWdpKNWQOQmhfxMCQ/SBVIO6+RkazI4y4dVeeP3KMP8nxfhzIbzTMNSkyynps/deZFjpDWqRhG+Dg==", "requires": { - "@nteract/messaging": "^7.0.10", - "@nteract/outputs": "^3.0.9", - "codemirror": "5.57.0", + "@nteract/messaging": "^7.0.19", + "@nteract/outputs": "^3.0.11", + "codemirror": "5.61.1", "rxjs": "^6.3.3" + }, + "dependencies": { + "@nteract/commutable": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@nteract/commutable/-/commutable-7.4.5.tgz", + "integrity": "sha512-RYqyMvkFt/04GQ9T+hGYgr9/LEy0dAYJ2QKn930TFX004KjfBT6Tt8VSLFyHWkXqPwyJ0jKMCJwqLcGOI/atqg==", + "requires": { + "immutable": "^4.0.0-rc.12", + "uuid": "^8.0.0" + } + }, + "@nteract/messaging": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/@nteract/messaging/-/messaging-7.0.19.tgz", + "integrity": "sha512-gRPMxJr741/BshrfCcPSbm5iVyRU2TKmAv9jeQzk0MZEGy+Y1A0REO+eptkt4Ma0OXlvDxON6JEDauk8+2xt4w==", + "requires": { + "@nteract/types": "^7.1.9", + "@types/uuid": "^8.0.0", + "lodash.clonedeep": "^4.5.0", + "rxjs": "^6.6.0", + "uuid": "^8.0.0" + } + }, + "@nteract/outputs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@nteract/outputs/-/outputs-3.0.11.tgz", + "integrity": "sha512-LeT9ViBf+fTPSubZ9dMe7128kg0rl1jIG54V0n2GiU5RuYnUz21FU0IOaLMPUfFMO1VyVEOW5jDc3PAQx5/Kwg==", + "requires": { + "@nteract/markdown": "^4.5.2", + "@nteract/mathjax": "^4.0.11", + "ansi-to-react": "^6.0.5", + "react-json-tree": "^0.12.1" + } + }, + "@nteract/types": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@nteract/types/-/types-7.1.9.tgz", + "integrity": "sha512-a7lGMWdjfz2QGlZbAiFHifU9Nhk9ntwg/iKUTMIMRPY1Wfs5UreHSMt+vZ8OY5HGjxicfHozBatGDKXeKXFHMQ==", + "requires": { + "@nteract/commutable": "^7.4.5", + "immutable": "^4.0.0-rc.12", + "rxjs": "^6.6.0", + "uuid": "^8.0.0" + } + }, + "react-base16-styling": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.7.0.tgz", + "integrity": "sha512-lTa/VSFdU6BOAj+FryOe7OTZ0OBP8GXPOnCS0QnZi7G3zhssWgIgwl0eUL77onXx/WqKPFndB3ZeC77QC/l4Dw==", + "requires": { + "base16": "^1.0.0", + "lodash.curry": "^4.1.1", + "lodash.flow": "^3.5.0", + "pure-color": "^1.3.0" + } + }, + "react-json-tree": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.12.1.tgz", + "integrity": "sha512-j6fkRY7ha9XMv1HPVakRCsvyFwHGR5AZuwO8naBBeZXnZbbLor5tpcUxS/8XD01+D1v7ZN5p+7LU+9V1uyASiQ==", + "requires": { + "prop-types": "^15.7.2", + "react-base16-styling": "^0.7.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } } }, "@nteract/epics": { @@ -5650,6 +5720,15 @@ "redux": "^4.0.0" } }, + "@types/react-splitter-layout": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/react-splitter-layout/-/react-splitter-layout-3.0.1.tgz", + "integrity": "sha512-NsKq32LdG11G/Uj+xo2QmC9S8YSe8JRtxkBhsBE7ODFs0zcnzNEqFAQirP0H7rPe2WFGiu+d/44xbHsew7QAJw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-table": { "version": "6.8.7", "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.7.tgz", @@ -8058,9 +8137,9 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codemirror": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz", - "integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg==" + "version": "5.61.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz", + "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==" }, "collapse-white-space": { "version": "1.0.6", @@ -17690,12 +17769,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -18499,9 +18572,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-es": { "version": "4.17.20", @@ -18728,9 +18801,9 @@ "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==" }, "marked": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.3.tgz", - "integrity": "sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.6.tgz", + "integrity": "sha512-S2mYj0FzTQa0dLddssqwRVW4EOJOVJ355Xm2Vcbm+LU7GQRGWvwbO5K87OaPSOux2AwTSgtPPaXmc8sDPrhn2A==", "dev": true }, "martinez-polygon-clipping": { @@ -21635,6 +21708,11 @@ "react-is": "^16.9.0" } }, + "react-splitter-layout": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz", + "integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA==" + }, "react-syntax-highlighter": { "version": "12.2.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz", @@ -24367,12 +24445,6 @@ "universalify": "^2.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -24388,9 +24460,9 @@ "dev": true }, "typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz", + "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==", "dev": true }, "typestyle": { diff --git a/package.json b/package.json index 811475c19..886b54c8f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@nteract/data-explorer": "8.0.3", "@nteract/directory-listing": "2.0.6", "@nteract/dropdown-menu": "1.0.1", - "@nteract/editor": "10.1.2", + "@nteract/editor": "10.1.12", "@nteract/fixtures": "2.3.0", "@nteract/iron-icons": "1.0.0", "@nteract/jupyter-widgets": "2.0.0", @@ -89,6 +89,7 @@ "react-i18next": "11.8.5", "react-notification-system": "0.2.17", "react-redux": "7.1.3", + "react-splitter-layout": "4.0.0", "redux": "4.0.4", "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", @@ -123,6 +124,7 @@ "@types/react-dom": "17.0.3", "@types/react-notification-system": "0.2.39", "@types/react-redux": "7.1.7", + "@types/react-splitter-layout": "3.0.1", "@types/sanitize-html": "1.27.2", "@types/sinon": "2.3.3", "@types/styled-components": "5.1.1", @@ -172,7 +174,7 @@ "tslint": "5.11.0", "tslint-microsoft-contrib": "6.0.0", "typedoc": "0.20.36", - "typescript": "4.2.4", + "typescript": "4.3.4", "url-loader": "1.1.1", "wait-on": "4.0.2", "webpack": "4.46.0", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 79edd0234..57b65c43e 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -158,16 +158,6 @@ export class DocumentsGridMetrics { public static DocumentEditorMaxWidthRatio: number = 0.4; } -export class ExplorerMetrics { - public static SplitterMinWidth: number = 240; - public static SplitterMaxWidth: number = 400; - public static CollapsedResourceTreeWidth: number = 36; -} - -export class SplitterMetrics { - public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth; -} - export class Areas { public static ResourceTree: string = "Resource Tree"; public static ContextualPane: string = "Contextual Pane"; diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 8f7e033f9..3a5a02365 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -5,7 +5,6 @@ import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; import { updateUserContext } from "../UserContext"; import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; -jest.mock("../ResourceProvider/ResourceProviderClient.ts"); const databaseId = "testDB"; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index f9f5b356d..2945f1288 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -111,7 +111,7 @@ export function queryDocuments( headers: response.headers, }; } - errorHandling(response, "querying documents", params); + await errorHandling(response, "querying documents", params); return undefined; }); } @@ -153,11 +153,11 @@ export function readDocument( ), }, }) - .then((response) => { + .then(async (response) => { if (response.ok) { return response.json(); } - return errorHandling(response, "reading document", params); + return await errorHandling(response, "reading document", params); }); } @@ -192,11 +192,11 @@ export function createDocument( ...authHeaders(), }, }) - .then((response) => { + .then(async (response) => { if (response.ok) { return response.json(); } - return errorHandling(response, "creating document", params); + return await errorHandling(response, "creating document", params); }); } @@ -238,11 +238,11 @@ export function updateDocument( [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), }, }) - .then((response) => { + .then(async (response) => { if (response.ok) { return response.json(); } - return errorHandling(response, "updating document", params); + return await errorHandling(response, "updating document", params); }); } @@ -278,11 +278,11 @@ export function deleteDocument(databaseId: string, collection: Collection, docum [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), }, }) - .then((response) => { + .then(async (response) => { if (response.ok) { return undefined; } - return errorHandling(response, "deleting document", params); + return await errorHandling(response, "deleting document", params); }); } @@ -325,11 +325,11 @@ export function createMongoCollectionWithProxy( }, } ) - .then((response) => { + .then(async (response) => { if (response.ok) { return response.json(); } - return errorHandling(response, "creating collection", mongoParams); + return await errorHandling(response, "creating collection", mongoParams); }); } diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index 23e1ee9e1..534d12879 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -1,19 +1,17 @@ -import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import * as _ from "underscore"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { useDatabases } from "../Explorer/useDatabases"; import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; -import * as QueryUtils from "../Utils/QueryUtils"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { createCollection } from "./dataAccess/createCollection"; import { createDocument } from "./dataAccess/createDocument"; import { deleteDocument } from "./dataAccess/deleteDocument"; import { queryDocuments } from "./dataAccess/queryDocuments"; -import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { handleError } from "./ErrorHandlingUtils"; export class QueriesClient { @@ -100,45 +98,35 @@ export class QueriesClient { const options: any = { enableCrossPartitionQuery: true }; const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); - const queryIterator: QueryIterator = queryDocuments( + const results = await queryDocuments( SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options - ); - const fetchQueries = async (firstItemIndex: number): Promise => - await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex); - return QueryUtils.queryAllPages(fetchQueries) - .then( - (results: ViewModels.QueryResults) => { - let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { - if (!document) { - return undefined; - } - const { id, resourceId, query, queryName } = document; - const parsedQuery: DataModels.Query = { - resourceId: resourceId, - queryName: queryName, - query: query, - id: id, - }; - try { - this.validateQuery(parsedQuery); - return parsedQuery; - } catch (error) { - return undefined; - } - }); - queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); - NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries"); - return Promise.resolve(queries); - }, - (error: any) => { - handleError(error, "getSavedQueries", "Failed to fetch saved queries"); - return Promise.reject(error); - } - ) - .finally(() => clearMessage()); + ).fetchAll(); + + let queries: DataModels.Query[] = _.map(results.resources, (document: DataModels.Query) => { + if (!document) { + return undefined; + } + const { id, resourceId, query, queryName } = document; + const parsedQuery: DataModels.Query = { + resourceId: resourceId, + queryName: queryName, + query: query, + id: id, + }; + try { + this.validateQuery(parsedQuery); + return parsedQuery; + } catch (error) { + return undefined; + } + }); + queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery); + NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries"); + clearMessage(); + return queries; } public async deleteQuery(query: DataModels.Query): Promise { @@ -189,7 +177,7 @@ export class QueriesClient { private findQueriesCollection(): ViewModels.Collection { const queriesDatabase: ViewModels.Database = _.find( - this.container.databases(), + useDatabases.getState().databases, (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName ); if (!queriesDatabase) { diff --git a/src/Common/Splitter.ts b/src/Common/Splitter.ts index cf2518812..1db6d3ba5 100644 --- a/src/Common/Splitter.ts +++ b/src/Common/Splitter.ts @@ -1,7 +1,3 @@ -import * as ko from "knockout"; - -import { SplitterMetrics } from "./Constants"; - export enum SplitterDirection { Horizontal = "horizontal", Vertical = "vertical", @@ -28,14 +24,12 @@ export class Splitter { public lastX!: number; public lastWidth!: number; - private isCollapsed: ko.Observable; private bounds: SplitterBounds; private direction: SplitterDirection; constructor(options: SplitterOptions) { this.splitterId = options.splitterId; this.leftSideId = options.leftId; - this.isCollapsed = ko.observable(false); this.bounds = options.bounds; this.direction = options.direction; this.initialize(); @@ -83,23 +77,4 @@ export class Splitter { }; private onResizeStop: JQueryUI.ResizableEvent = () => $("iframe").css("pointer-events", "auto"); - - public collapseLeft() { - this.lastX = $(this.splitter).position().left; - this.lastWidth = $(this.leftSide).width(); - $(this.splitter).css("left", SplitterMetrics.CollapsedPositionLeft); - $(this.leftSide).css("width", ""); - $(this.leftSide).resizable("option", "disabled", true).removeClass("ui-resizable-disabled"); // remove class so splitter is visible - $(this.splitter).removeClass("ui-resizable-e"); - this.isCollapsed(true); - } - - public expandLeft() { - $(this.splitter).addClass("ui-resizable-e"); - $(this.leftSide).css("width", this.lastWidth); - $(this.splitter).css("left", this.lastX); - $(this.splitter).css("left", ""); // this ensures the splitter's position is not fixed and enables movement during resizing - $(this.leftSide).resizable("enable"); - this.isCollapsed(false); - } } diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 5f64cbe81..e83a4e9ec 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -120,6 +120,14 @@ export async function initializeConfiguration(): Promise { const armAPIVersion = params.get("armAPIVersion") || ""; updateConfigContext({ armAPIVersion }); } + if (params.has("armEndpoint")) { + const ARM_ENDPOINT = params.get("armEndpoint") || ""; + updateConfigContext({ ARM_ENDPOINT }); + } + if (params.has("aadEndpoint")) { + const AAD_ENDPOINT = params.get("aadEndpoint") || ""; + updateConfigContext({ AAD_ENDPOINT }); + } if (params.has("platform")) { const platform = params.get("platform"); switch (platform) { diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index c44a86041..210d34075 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -6,7 +6,7 @@ import { UserDefinedFunctionDefinition, } from "@azure/cosmos"; import Explorer from "../Explorer/Explorer"; -import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData"; import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient"; import ConflictId from "../Explorer/Tree/ConflictId"; import DocumentId from "../Explorer/Tree/DocumentId"; @@ -89,7 +89,6 @@ export interface Database extends TreeNode { selectedSubnodeKind: ko.Observable; - selectDatabase(): void; expandDatabase(): Promise; collapseDatabase(): void; @@ -275,7 +274,6 @@ export interface TabOptions { tabKind: CollectionTabKind; title: string; tabPath: string; - hashLocation: string; isTabsContentExpanded?: ko.Observable; onLoadStartKey?: number; @@ -286,6 +284,7 @@ export interface TabOptions { rid?: string; node?: TreeNode; theme?: string; + index?: number; } export interface DocumentsTabOptions extends TabOptions { diff --git a/src/Explorer/ContextMenuButtonFactory.ts b/src/Explorer/ContextMenuButtonFactory.ts deleted file mode 100644 index 42eac8d44..000000000 --- a/src/Explorer/ContextMenuButtonFactory.ts +++ /dev/null @@ -1,172 +0,0 @@ -import AddCollectionIcon from "../../images/AddCollection.svg"; -import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg"; -import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg"; -import AddTriggerIcon from "../../images/AddTrigger.svg"; -import AddUdfIcon from "../../images/AddUdf.svg"; -import DeleteCollectionIcon from "../../images/DeleteCollection.svg"; -import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg"; -import DeleteSprocIcon from "../../images/DeleteSproc.svg"; -import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; -import DeleteUDFIcon from "../../images/DeleteUDF.svg"; -import HostedTerminalIcon from "../../images/Hosted-Terminal.svg"; -import * as ViewModels from "../Contracts/ViewModels"; -import { userContext } from "../UserContext"; -import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; -import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent"; -import Explorer from "./Explorer"; -import StoredProcedure from "./Tree/StoredProcedure"; -import Trigger from "./Tree/Trigger"; -import UserDefinedFunction from "./Tree/UserDefinedFunction"; -export interface CollectionContextMenuButtonParams { - databaseId: string; - collectionId: string; -} - -export interface DatabaseContextMenuButtonParams { - databaseId: string; -} -/** - * New resource tree (in ReactJS) - */ -export class ResourceTreeContextMenuButtonFactory { - public static createDatabaseContextMenu(container: Explorer, databaseId: string): TreeNodeMenuItem[] { - const items: TreeNodeMenuItem[] = [ - { - iconSrc: AddCollectionIcon, - onClick: () => container.onNewCollectionClicked(databaseId), - label: `New ${getCollectionName()}`, - }, - ]; - - if (userContext.apiType !== "Tables") { - items.push({ - iconSrc: DeleteDatabaseIcon, - onClick: () => container.openDeleteDatabaseConfirmationPane(), - label: `Delete ${getDatabaseName()}`, - styleClass: "deleteDatabaseMenuItem", - }); - } - return items; - } - - public static createCollectionContextMenuButton( - container: Explorer, - selectedCollection: ViewModels.Collection - ): TreeNodeMenuItem[] { - const items: TreeNodeMenuItem[] = []; - if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { - items.push({ - iconSrc: AddSqlQueryIcon, - onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null), - label: "New SQL Query", - }); - } - - if (userContext.apiType === "Mongo") { - items.push({ - iconSrc: AddSqlQueryIcon, - onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null), - label: "New Query", - }); - - items.push({ - iconSrc: HostedTerminalIcon, - onClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - if (container.isShellEnabled()) { - container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); - } else { - selectedCollection && selectedCollection.onNewMongoShellClick(); - } - }, - label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell", - }); - } - - if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { - items.push({ - iconSrc: AddStoredProcedureIcon, - onClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null); - }, - label: "New Stored Procedure", - }); - - items.push({ - iconSrc: AddUdfIcon, - onClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null); - }, - label: "New UDF", - }); - - items.push({ - iconSrc: AddTriggerIcon, - onClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null); - }, - label: "New Trigger", - }); - } - - items.push({ - iconSrc: DeleteCollectionIcon, - onClick: () => container.openDeleteCollectionConfirmationPane(), - label: `Delete ${getCollectionName()}`, - styleClass: "deleteCollectionMenuItem", - }); - - return items; - } - - public static createStoreProcedureContextMenuItems( - container: Explorer, - storedProcedure: StoredProcedure - ): TreeNodeMenuItem[] { - if (userContext.apiType === "Cassandra") { - return []; - } - - return [ - { - iconSrc: DeleteSprocIcon, - onClick: () => storedProcedure.delete(), - label: "Delete Store Procedure", - }, - ]; - } - - public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] { - if (userContext.apiType === "Cassandra") { - return []; - } - - return [ - { - iconSrc: DeleteTriggerIcon, - onClick: () => trigger.delete(), - label: "Delete Trigger", - }, - ]; - } - - public static createUserDefinedFunctionContextMenuItems( - container: Explorer, - userDefinedFunction: UserDefinedFunction - ): TreeNodeMenuItem[] { - if (userContext.apiType === "Cassandra") { - return []; - } - - return [ - { - iconSrc: DeleteUDFIcon, - onClick: () => userDefinedFunction.delete(), - label: "Delete User Defined Function", - }, - ]; - } -} diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx new file mode 100644 index 000000000..4b1b52054 --- /dev/null +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -0,0 +1,182 @@ +import React from "react"; +import AddCollectionIcon from "../../images/AddCollection.svg"; +import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg"; +import AddStoredProcedureIcon from "../../images/AddStoredProcedure.svg"; +import AddTriggerIcon from "../../images/AddTrigger.svg"; +import AddUdfIcon from "../../images/AddUdf.svg"; +import DeleteCollectionIcon from "../../images/DeleteCollection.svg"; +import DeleteDatabaseIcon from "../../images/DeleteDatabase.svg"; +import DeleteSprocIcon from "../../images/DeleteSproc.svg"; +import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; +import DeleteUDFIcon from "../../images/DeleteUDF.svg"; +import HostedTerminalIcon from "../../images/Hosted-Terminal.svg"; +import * as ViewModels from "../Contracts/ViewModels"; +import { useSidePanel } from "../hooks/useSidePanel"; +import { userContext } from "../UserContext"; +import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; +import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent"; +import Explorer from "./Explorer"; +import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; +import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; +import StoredProcedure from "./Tree/StoredProcedure"; +import Trigger from "./Tree/Trigger"; +import UserDefinedFunction from "./Tree/UserDefinedFunction"; +import { useSelectedNode } from "./useSelectedNode"; + +export interface CollectionContextMenuButtonParams { + databaseId: string; + collectionId: string; +} + +export interface DatabaseContextMenuButtonParams { + databaseId: string; +} +/** + * New resource tree (in ReactJS) + */ +export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => { + const items: TreeNodeMenuItem[] = [ + { + iconSrc: AddCollectionIcon, + onClick: () => container.onNewCollectionClicked(databaseId), + label: `New ${getCollectionName()}`, + }, + ]; + + if (userContext.apiType !== "Tables") { + items.push({ + iconSrc: DeleteDatabaseIcon, + onClick: () => + useSidePanel + .getState() + .openSidePanel("Delete " + getDatabaseName(), ), + label: `Delete ${getDatabaseName()}`, + styleClass: "deleteDatabaseMenuItem", + }); + } + return items; +}; + +export const createCollectionContextMenuButton = ( + container: Explorer, + selectedCollection: ViewModels.Collection +): TreeNodeMenuItem[] => { + const items: TreeNodeMenuItem[] = []; + if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { + items.push({ + iconSrc: AddSqlQueryIcon, + onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined), + label: "New SQL Query", + }); + } + + if (userContext.apiType === "Mongo") { + items.push({ + iconSrc: AddSqlQueryIcon, + onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined), + label: "New Query", + }); + + items.push({ + iconSrc: HostedTerminalIcon, + onClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + if (container.isShellEnabled()) { + container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); + } else { + selectedCollection && selectedCollection.onNewMongoShellClick(); + } + }, + label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell", + }); + } + + if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { + items.push({ + iconSrc: AddStoredProcedureIcon, + onClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined); + }, + label: "New Stored Procedure", + }); + + items.push({ + iconSrc: AddUdfIcon, + onClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined); + }, + label: "New UDF", + }); + + items.push({ + iconSrc: AddTriggerIcon, + onClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined); + }, + label: "New Trigger", + }); + } + + items.push({ + iconSrc: DeleteCollectionIcon, + onClick: () => + useSidePanel + .getState() + .openSidePanel("Delete " + getCollectionName(), ), + label: `Delete ${getCollectionName()}`, + styleClass: "deleteCollectionMenuItem", + }); + + return items; +}; + +export const createStoreProcedureContextMenuItems = ( + container: Explorer, + storedProcedure: StoredProcedure +): TreeNodeMenuItem[] => { + if (userContext.apiType === "Cassandra") { + return []; + } + + return [ + { + iconSrc: DeleteSprocIcon, + onClick: () => storedProcedure.delete(), + label: "Delete Store Procedure", + }, + ]; +}; + +export const createTriggerContextMenuItems = (container: Explorer, trigger: Trigger): TreeNodeMenuItem[] => { + if (userContext.apiType === "Cassandra") { + return []; + } + + return [ + { + iconSrc: DeleteTriggerIcon, + onClick: () => trigger.delete(), + label: "Delete Trigger", + }, + ]; +}; + +export const createUserDefinedFunctionContextMenuItems = ( + container: Explorer, + userDefinedFunction: UserDefinedFunction +): TreeNodeMenuItem[] => { + if (userContext.apiType === "Cassandra") { + return []; + } + + return [ + { + iconSrc: DeleteUDFIcon, + onClick: () => userDefinedFunction.delete(), + label: "Delete User Defined Function", + }, + ]; +}; diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 71273ed20..e97cf5da4 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -1,6 +1,11 @@ +import { Spinner, SpinnerSize } from "@fluentui/react"; import * as React from "react"; import { loadMonaco, monaco } from "../../LazyMonaco"; +// import "./EditorReact.less"; +interface EditorReactStates { + showEditor: boolean; +} export interface EditorReactProps { language: string; content: string; @@ -12,22 +17,26 @@ export interface EditorReactProps { theme?: string; // Monaco editor theme } -export class EditorReact extends React.Component { +export class EditorReact extends React.Component { private rootNode: HTMLElement; private editor: monaco.editor.IStandaloneCodeEditor; private selectionListener: monaco.IDisposable; public constructor(props: EditorReactProps) { super(props); + this.state = { + showEditor: false, + }; } public componentDidMount(): void { this.createEditor(this.configureEditor.bind(this)); } - public shouldComponentUpdate(): boolean { - // Prevents component re-rendering - return false; + public componentDidUpdate(previous: EditorReactProps) { + if (this.props.content !== previous.content) { + this.editor.setValue(this.props.content); + } } public componentWillUnmount(): void { @@ -35,7 +44,12 @@ export class EditorReact extends React.Component { } public render(): JSX.Element { - return
this.setRef(elt)} />; + return ( + + {!this.state.showEditor && } +
this.setRef(elt)} /> + + ); } protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { @@ -76,6 +90,12 @@ export class EditorReact extends React.Component { this.rootNode.innerHTML = ""; const monaco = await loadMonaco(); createCallback(monaco.editor.create(this.rootNode, options)); + + if (this.rootNode.innerHTML) { + this.setState({ + showEditor: true, + }); + } } private setRef(element: HTMLElement): void { diff --git a/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx b/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx deleted file mode 100644 index aa8cabde5..000000000 --- a/src/Explorer/Controls/GitHub/GitHubReposComponentAdapter.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent"; - -export class GitHubReposComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - constructor(private props: GitHubReposComponentProps) { - this.parameters = ko.observable(Date.now()); - } - - public renderComponent(): JSX.Element { - return ; - } - - public triggerRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx index 805317627..3bbc6a538 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx @@ -55,7 +55,7 @@ export class NotebookViewerComponent databaseAccountName: undefined, defaultExperience: "NotebookViewer", isReadOnly: true, - cellEditorType: "monaco", + cellEditorType: "codemirror", autoSaveInterval: 365 * 24 * 3600 * 1000, // There is no way to turn off auto-save, set to 1 year contentProvider: contents.JupyterContentProvider, // NotebookViewer only knows how to talk to Jupyter contents API }); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index e4cee955c..d14fe1cc4 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -38,7 +38,6 @@ describe("SettingsComponent", () => { title: "Scale & Settings", tabPath: "", node: undefined, - hashLocation: "settings", }), }; @@ -127,7 +126,6 @@ describe("SettingsComponent", () => { isDatabaseExpanded: undefined, isDatabaseShared: ko.computed(() => true), selectedSubnodeKind: undefined, - selectDatabase: undefined, expandDatabase: undefined, collapseDatabase: undefined, loadCollections: undefined, diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index f2ba989ff..cc7cc15ba 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -16,8 +16,8 @@ import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/T import { userContext } from "../../../UserContext"; import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; +import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; -import Explorer from "../../Explorer"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import "./SettingsComponent.less"; @@ -110,6 +110,7 @@ export interface SettingsComponentState { initialNotification: DataModels.Notification; selectedTab: SettingsV2TabTypes; + offerLoaded: boolean; } export class SettingsComponent extends React.Component { @@ -122,7 +123,6 @@ export class SettingsComponent extends React.Component - this.container && userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection); + userContext.apiType === "Cassandra" && hasDatabaseSharedThroughput(this.collection); public hasConflictResolution = (): boolean => userContext?.databaseAccount?.properties?.enableMultipleWriteLocations && @@ -372,6 +372,34 @@ export class SettingsComponent extends React.Component { let finalIndexes: MongoIndex[] = []; this.state.currentMongoIndexes?.map((mongoIndex: MongoIndex, index: number) => { @@ -884,7 +912,6 @@ export class SettingsComponent extends React.Component; + } + const subSettingsComponentProps: SubSettingsComponentProps = { collection: this.collection, - container: this.container, isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled, changeFeedPolicyVisible: this.changeFeedPolicyVisible, timeToLive: this.state.timeToLive, @@ -965,7 +995,6 @@ export class SettingsComponent extends React.Component; - - constructor(private props: SettingsComponentProps) {} - - public renderComponent(): JSX.Element { - return this.parameters() ? : <>; - } -} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.test.tsx index 4d5f7fd6e..9ba23c675 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.test.tsx @@ -1,13 +1,12 @@ import { shallow } from "enzyme"; import React from "react"; -import { ConflictResolutionComponentProps, ConflictResolutionComponent } from "./ConflictResolutionComponent"; -import { container, collection } from "../TestUtils"; import * as DataModels from "../../../../Contracts/DataModels"; +import { collection } from "../TestUtils"; +import { ConflictResolutionComponent, ConflictResolutionComponentProps } from "./ConflictResolutionComponent"; describe("ConflictResolutionComponent", () => { const baseProps: ConflictResolutionComponentProps = { collection: collection, - container: container, conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom, conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode.Custom, onConflictResolutionPolicyModeChange: () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.tsx index ec23ac0db..c6ca020a5 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ConflictResolutionComponent.tsx @@ -1,21 +1,19 @@ +import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } from "@fluentui/react"; import * as React from "react"; -import * as ViewModels from "../../../../Contracts/ViewModels"; import * as DataModels from "../../../../Contracts/DataModels"; -import Explorer from "../../../Explorer"; +import * as ViewModels from "../../../../Contracts/ViewModels"; import { - getTextFieldStyles, - conflictResolutionLwwTooltip, conflictResolutionCustomToolTip, - subComponentStackProps, + conflictResolutionLwwTooltip, getChoiceGroupStyles, + getTextFieldStyles, + subComponentStackProps, } from "../SettingsRenderUtils"; -import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "@fluentui/react"; -import { ToolTipLabelComponent } from "./ToolTipLabelComponent"; import { isDirty } from "../SettingsUtils"; +import { ToolTipLabelComponent } from "./ToolTipLabelComponent"; export interface ConflictResolutionComponentProps { collection: ViewModels.Collection; - container: Explorer; conflictResolutionPolicyMode: DataModels.ConflictResolutionMode; conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode; onConflictResolutionPolicyModeChange: (newMode: DataModels.ConflictResolutionMode) => void; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx index 4ba0e0427..eb93add8c 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx @@ -7,20 +7,17 @@ import * as SharedConstants from "../../../../Shared/Constants"; import { updateUserContext } from "../../../../UserContext"; import Explorer from "../../../Explorer"; import { throughputUnit } from "../SettingsRenderUtils"; -import { collection, container } from "../TestUtils"; +import { collection } from "../TestUtils"; import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent"; import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component"; describe("ScaleComponent", () => { const nonNationalCloudContainer = new Explorer(); - nonNationalCloudContainer.isRunningOnNationalCloud = () => false; - const targetThroughput = 6000; const baseProps: ScaleComponentProps = { collection: collection, database: undefined, - container: container, isFixedContainer: false, onThroughputChange: () => { return; @@ -111,7 +108,7 @@ describe("ScaleComponent", () => { let scaleComponent = new ScaleComponent(baseProps); expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)"); - let newProps = { ...baseProps, container: nonNationalCloudContainer }; + let newProps = { ...baseProps }; scaleComponent = new ScaleComponent(newProps); expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)"); @@ -124,7 +121,7 @@ describe("ScaleComponent", () => { let scaleComponent = new ScaleComponent(baseProps); expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true); - const newProps = { ...baseProps, container: nonNationalCloudContainer }; + const newProps = { ...baseProps }; scaleComponent = new ScaleComponent(newProps); expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true); }); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index e73469101..98914e33a 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -7,7 +7,7 @@ import * as ViewModels from "../../../../Contracts/ViewModels"; import * as SharedConstants from "../../../../Shared/Constants"; import { userContext } from "../../../../UserContext"; import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils"; -import Explorer from "../../../Explorer"; +import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils"; import { getTextFieldStyles, getThroughputApplyLongDelayMessage, @@ -23,7 +23,6 @@ import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents export interface ScaleComponentProps { collection: ViewModels.Collection; database: ViewModels.Database; - container: Explorer; isFixedContainer: boolean; onThroughputChange: (newThroughput: number) => void; throughput: number; @@ -109,11 +108,7 @@ export class ScaleComponent extends React.Component { }; public canThroughputExceedMaximumValue = (): boolean => { - return ( - !this.props.isFixedContainer && - configContext.platform === Platform.Portal && - !this.props.container.isRunningOnNationalCloud() - ); + return !this.props.isFixedContainer && configContext.platform === Platform.Portal && !isRunningOnNationalCloud(); }; public getInitialNotificationElement = (): JSX.Element => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx index 2d25f9da7..664bc3968 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx @@ -4,14 +4,12 @@ import { DatabaseAccount } from "../../../../Contracts/DataModels"; import { updateUserContext } from "../../../../UserContext"; import Explorer from "../../../Explorer"; import { ChangeFeedPolicyState, GeospatialConfigType, TtlOff, TtlOn, TtlOnNoDefault, TtlType } from "../SettingsUtils"; -import { collection, container } from "../TestUtils"; +import { collection } from "../TestUtils"; import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent"; describe("SubSettingsComponent", () => { const baseProps: SubSettingsComponentProps = { collection: collection, - container: container, - timeToLive: TtlType.On, timeToLiveBaseline: TtlType.On, onTtlChange: () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx index 0338cc667..84913b022 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx @@ -2,7 +2,6 @@ import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text, import * as React from "react"; import * as ViewModels from "../../../../Contracts/ViewModels"; import { userContext } from "../../../../UserContext"; -import Explorer from "../../../Explorer"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { changeFeedPolicyToolTip, @@ -28,8 +27,6 @@ import { ToolTipLabelComponent } from "./ToolTipLabelComponent"; export interface SubSettingsComponentProps { collection: ViewModels.Collection; - container: Explorer; - timeToLive: TtlType; timeToLiveBaseline: TtlType; diff --git a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx index 81c025c1c..1a7e4d79b 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.test.tsx @@ -36,7 +36,6 @@ describe("SettingsUtils", () => { isDatabaseExpanded: ko.observable(false), isDatabaseShared: ko.computed(() => true), selectedSubnodeKind: ko.observable(undefined), - selectDatabase: undefined, expandDatabase: undefined, collapseDatabase: undefined, loadCollections: undefined, diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index c5db2d2af..e34b56fb7 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -30,17 +30,11 @@ exports[`SettingsComponent renders 1`] = ` "container": Explorer { "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "collapsedResourceTreeWidth": 36, - "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isResourceTokenCollectionNodeSelected": [Function], "isSchemaEnabled": [Function], - "isServerlessEnabled": [Function], "isShellEnabled": [Function], "isSynapseLinkUpdating": [Function], "isTabsContentExpanded": [Function], @@ -55,38 +49,16 @@ exports[`SettingsComponent renders 1`] = ` }, "refreshNotebookList": [Function], "resourceTokenCollection": [Function], - "resourceTokenCollectionId": [Function], - "resourceTokenDatabaseId": [Function], - "resourceTokenPartitionKey": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": Map {}, - "koSubsCollectionIdMap": Map {}, - "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "container": [Circular], "parameters": [Function], }, - "selectedDatabaseId": [Function], - "selectedNode": [Function], - "setInProgressConsoleDataIdToBeDeleted": undefined, - "setNotificationConsoleData": undefined, "sparkClusterConnectionInfo": [Function], - "splitter": Splitter { - "bounds": Object { - "max": 400, - "min": 240, - }, - "direction": "vertical", - "isCollapsed": [Function], - "leftSideId": "resourcetree", - "onResizeStart": [Function], - "onResizeStop": [Function], - "splitterId": "h_splitter1", - }, "tabsManager": TabsManager { "activeTab": [Function], "openedTabs": [Function], @@ -110,73 +82,6 @@ exports[`SettingsComponent renders 1`] = ` "usageSizeInKB": [Function], } } - container={ - Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "collapsedResourceTreeWidth": 36, - "databases": [Function], - "isAccountReady": [Function], - "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], - "isNotebookEnabled": [Function], - "isNotebooksEnabledForAccount": [Function], - "isResourceTokenCollectionNodeSelected": [Function], - "isSchemaEnabled": [Function], - "isServerlessEnabled": [Function], - "isShellEnabled": [Function], - "isSynapseLinkUpdating": [Function], - "isTabsContentExpanded": [Function], - "memoryUsageInfo": [Function], - "notebookBasePath": [Function], - "notebookServerInfo": [Function], - "onRefreshDatabasesKeyPress": [Function], - "onRefreshResourcesClick": [Function], - "provideFeedbackEmail": [Function], - "queriesClient": QueriesClient { - "container": [Circular], - }, - "refreshNotebookList": [Function], - "resourceTokenCollection": [Function], - "resourceTokenCollectionId": [Function], - "resourceTokenDatabaseId": [Function], - "resourceTokenPartitionKey": [Function], - "resourceTree": ResourceTreeAdapter { - "container": [Circular], - "copyNotebook": [Function], - "databaseCollectionIdMap": Map {}, - "koSubsCollectionIdMap": Map {}, - "koSubsDatabaseIdMap": Map {}, - "parameters": [Function], - }, - "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { - "container": [Circular], - "parameters": [Function], - }, - "selectedDatabaseId": [Function], - "selectedNode": [Function], - "setInProgressConsoleDataIdToBeDeleted": undefined, - "setNotificationConsoleData": undefined, - "sparkClusterConnectionInfo": [Function], - "splitter": Splitter { - "bounds": Object { - "max": 400, - "min": 240, - }, - "direction": "vertical", - "isCollapsed": [Function], - "leftSideId": "resourcetree", - "onResizeStart": [Function], - "onResizeStop": [Function], - "splitterId": "h_splitter1", - }, - "tabsManager": TabsManager { - "activeTab": [Function], - "openedTabs": [Function], - }, - } - } isAutoPilotSelected={false} isFixedContainer={false} onAutoPilotSelected={[Function]} @@ -211,17 +116,11 @@ exports[`SettingsComponent renders 1`] = ` "container": Explorer { "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "collapsedResourceTreeWidth": 36, - "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isResourceTokenCollectionNodeSelected": [Function], "isSchemaEnabled": [Function], - "isServerlessEnabled": [Function], "isShellEnabled": [Function], "isSynapseLinkUpdating": [Function], "isTabsContentExpanded": [Function], @@ -236,38 +135,16 @@ exports[`SettingsComponent renders 1`] = ` }, "refreshNotebookList": [Function], "resourceTokenCollection": [Function], - "resourceTokenCollectionId": [Function], - "resourceTokenDatabaseId": [Function], - "resourceTokenPartitionKey": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": Map {}, - "koSubsCollectionIdMap": Map {}, - "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "container": [Circular], "parameters": [Function], }, - "selectedDatabaseId": [Function], - "selectedNode": [Function], - "setInProgressConsoleDataIdToBeDeleted": undefined, - "setNotificationConsoleData": undefined, "sparkClusterConnectionInfo": [Function], - "splitter": Splitter { - "bounds": Object { - "max": 400, - "min": 240, - }, - "direction": "vertical", - "isCollapsed": [Function], - "leftSideId": "resourcetree", - "onResizeStart": [Function], - "onResizeStop": [Function], - "splitterId": "h_splitter1", - }, "tabsManager": TabsManager { "activeTab": [Function], "openedTabs": [Function], @@ -291,73 +168,6 @@ exports[`SettingsComponent renders 1`] = ` "usageSizeInKB": [Function], } } - container={ - Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "collapsedResourceTreeWidth": 36, - "databases": [Function], - "isAccountReady": [Function], - "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], - "isNotebookEnabled": [Function], - "isNotebooksEnabledForAccount": [Function], - "isResourceTokenCollectionNodeSelected": [Function], - "isSchemaEnabled": [Function], - "isServerlessEnabled": [Function], - "isShellEnabled": [Function], - "isSynapseLinkUpdating": [Function], - "isTabsContentExpanded": [Function], - "memoryUsageInfo": [Function], - "notebookBasePath": [Function], - "notebookServerInfo": [Function], - "onRefreshDatabasesKeyPress": [Function], - "onRefreshResourcesClick": [Function], - "provideFeedbackEmail": [Function], - "queriesClient": QueriesClient { - "container": [Circular], - }, - "refreshNotebookList": [Function], - "resourceTokenCollection": [Function], - "resourceTokenCollectionId": [Function], - "resourceTokenDatabaseId": [Function], - "resourceTokenPartitionKey": [Function], - "resourceTree": ResourceTreeAdapter { - "container": [Circular], - "copyNotebook": [Function], - "databaseCollectionIdMap": Map {}, - "koSubsCollectionIdMap": Map {}, - "koSubsDatabaseIdMap": Map {}, - "parameters": [Function], - }, - "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { - "container": [Circular], - "parameters": [Function], - }, - "selectedDatabaseId": [Function], - "selectedNode": [Function], - "setInProgressConsoleDataIdToBeDeleted": undefined, - "setNotificationConsoleData": undefined, - "sparkClusterConnectionInfo": [Function], - "splitter": Splitter { - "bounds": Object { - "max": 400, - "min": 240, - }, - "direction": "vertical", - "isCollapsed": [Function], - "leftSideId": "resourcetree", - "onResizeStart": [Function], - "onResizeStop": [Function], - "splitterId": "h_splitter1", - }, - "tabsManager": TabsManager { - "activeTab": [Function], - "openedTabs": [Function], - }, - } - } geospatialConfigType="Geometry" geospatialConfigTypeBaseline="Geometry" isAnalyticalStorageEnabled={false} diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx index dd7127daa..6c5ceb817 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx @@ -58,7 +58,7 @@ export interface TreeComponentProps { export class TreeComponent extends React.Component { public render(): JSX.Element { return ( -
+
); @@ -172,6 +172,7 @@ export class TreeNodeComponent extends React.Component) => this.onNodeClick(event, node)} onKeyPress={(event: React.KeyboardEvent) => this.onNodeKeyPress(event, node)} + role="treeitem" >
{ - const createExplorerStub = (database: ViewModels.Database): Explorer => { - const explorerStub = {} as Explorer; - explorerStub.databases = ko.observableArray([database]); - explorerStub.findDatabaseWithId = () => database; - explorerStub.refreshAllDatabases = () => Q.resolve(); - return explorerStub; - }; + let explorerStub: Explorer; + + beforeAll(() => { + explorerStub = { + refreshAllDatabases: () => {}, + } as Explorer; + }); beforeEach(() => { (createDocument as jest.Mock).mockResolvedValue(undefined); @@ -59,8 +59,7 @@ describe("ContainerSampleGenerator", () => { loadCollections: () => {}, } as ViewModels.Database; database.findCollectionWithId = () => collection; - - const explorerStub = createExplorerStub(database); + useDatabases.getState().addDatabases([database]); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); generator.setData(sampleData); @@ -108,8 +107,8 @@ describe("ContainerSampleGenerator", () => { } as ViewModels.Database; database.findCollectionWithId = () => collection; collection.databaseId = database.id(); + useDatabases.getState().addDatabases([database]); - const explorerStub = createExplorerStub(database); updateUserContext({ databaseAccount: { properties: { @@ -126,7 +125,6 @@ describe("ContainerSampleGenerator", () => { it("should not create any sample for Mongo API account", async () => { const experience = "Sample generation not supported for this API Mongo"; - const explorerStub = createExplorerStub(undefined); updateUserContext({ databaseAccount: { properties: { @@ -141,7 +139,6 @@ describe("ContainerSampleGenerator", () => { it("should not create any sample for Table API account", async () => { const experience = "Sample generation not supported for this API Tables"; - const explorerStub = createExplorerStub(undefined); updateUserContext({ databaseAccount: { properties: { @@ -163,7 +160,6 @@ describe("ContainerSampleGenerator", () => { }, } as DatabaseAccount, }); - const explorerStub = createExplorerStub(undefined); // Rejects with error that contains experience await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); }); diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index dd9a4adb9..44906151d 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -7,6 +7,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils" import GraphTab from ".././Tabs/GraphTab"; import Explorer from "../Explorer"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; +import { useDatabases } from "../useDatabases"; interface SampleDataFile extends DataModels.CreateCollectionParams { data: any[]; @@ -59,7 +60,7 @@ export class ContainerSampleGenerator { await createCollection(createRequest); await this.container.refreshAllDatabases(); - const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId); + const database = useDatabases.getState().findDatabaseWithId(this.sampleDataFile.databaseId); if (!database) { return undefined; } diff --git a/src/Explorer/DataSamples/DataSamplesUtil.test.ts b/src/Explorer/DataSamples/DataSamplesUtil.test.ts index 9a35158f5..f8ff6f8e5 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.test.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.test.ts @@ -2,6 +2,7 @@ import * as ko from "knockout"; import * as sinon from "sinon"; import { Collection, Database } from "../../Contracts/ViewModels"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { DataSamplesUtil } from "./DataSamplesUtil"; @@ -16,8 +17,8 @@ describe("DataSampleUtils", () => { collections: ko.observableArray([collection]), } as Database; const explorer = {} as Explorer; - explorer.databases = ko.observableArray([database]); explorer.showOkModalDialog = () => {}; + useDatabases.getState().addDatabases([database]); const dataSamplesUtil = new DataSamplesUtil(explorer); const fakeGenerator = sinon.createStubInstance(ContainerSampleGenerator as any); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index 63b35cfff..4007608c0 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -2,6 +2,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { userContext } from "../../UserContext"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; export class DataSamplesUtil { @@ -17,7 +18,7 @@ export class DataSamplesUtil { const databaseName = generator.getDatabaseId(); const containerName = generator.getCollectionId(); - if (this.hasContainer(databaseName, containerName, this.container.databases())) { + if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleError(msg); diff --git a/src/Explorer/Explorer.test.tsx b/src/Explorer/Explorer.test.tsx deleted file mode 100644 index ef595252a..000000000 --- a/src/Explorer/Explorer.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -jest.mock("./../Common/dataAccess/deleteDatabase"); -jest.mock("./../Shared/Telemetry/TelemetryProcessor"); -import * as ko from "knockout"; -import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase"; -import * as ViewModels from "./../Contracts/ViewModels"; -import Explorer from "./Explorer"; - -describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => { - let explorer: Explorer; - beforeAll(() => { - (deleteDatabase as jest.Mock).mockResolvedValue(undefined); - }); - - beforeEach(() => { - explorer = new Explorer(); - }); - - it("should be true if only 1 database", () => { - const database = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastDatabase()).toBe(true); - }); - - it("should be false if only 2 databases", () => { - const database = {} as ViewModels.Database; - const database2 = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database, database2]); - expect(explorer.isLastDatabase()).toBe(false); - }); - - it("should be false if not last empty database", () => { - const database = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastNonEmptyDatabase()).toBe(false); - }); - - it("should be true if last non empty database", () => { - const database = {} as ViewModels.Database; - database.collections = ko.observableArray([{} as ViewModels.Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastNonEmptyDatabase()).toBe(true); - }); -}); diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index f5368679b..055868a24 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -6,28 +6,30 @@ import _ from "underscore"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { ExplorerMetrics } from "../Common/Constants"; import { readCollection } from "../Common/dataAccess/readCollection"; import { readDatabases } from "../Common/dataAccess/readDatabases"; import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; -import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter"; -import { configContext, Platform } from "../ConfigContext"; +import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { useSidePanel } from "../hooks/useSidePanel"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; -import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; -import { RouteHandler } from "../RouteHandlers/RouteHandler"; import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../UserContext"; import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { + get as getWorkspace, + listByDatabaseAccount, + listConnectionInfo, + start, +} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; @@ -38,7 +40,6 @@ import * as ComponentRegisterer from "./ComponentRegisterer"; import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent"; import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import { SnapshotRequest } from "./Notebook/NotebookComponent/types"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; @@ -49,24 +50,15 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; -import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; -import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; -import { SettingsPane } from "./Panes/SettingsPane/SettingsPane"; import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel"; -import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel"; -import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; -import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel"; -import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; -import QueryTablesTab from "./Tabs/QueryTablesTab"; import { TabsManager } from "./Tabs/TabsManager"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; @@ -74,44 +66,28 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import StoredProcedure from "./Tree/StoredProcedure"; +import { useDatabases } from "./useDatabases"; +import { useSelectedNode } from "./useSelectedNode"; BindingHandlersRegisterer.registerBindingHandlers(); // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import var tmp = ComponentRegisterer; export interface ExplorerParams { - setNotificationConsoleData: (consoleData: ConsoleData) => void; - setInProgressConsoleDataIdToBeDeleted: (id: string) => void; tabsManager: TabsManager; } export default class Explorer { - public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; - public isFixedCollectionWithSharedThroughputSupported: ko.Computed; - public isServerlessEnabled: ko.Computed; public isAccountReady: ko.Observable; - public canSaveQueries: ko.Computed; public queriesClient: QueriesClient; public tableDataClient: TableDataClient; - public splitter: Splitter; - - private setNotificationConsoleData: (consoleData: ConsoleData) => void; - private setInProgressConsoleDataIdToBeDeleted: (id: string) => void; // Resource Tree - public databases: ko.ObservableArray; - public selectedDatabaseId: ko.Computed; - public selectedCollectionId: ko.Computed; - public selectedNode: ko.Observable; private resourceTree: ResourceTreeAdapter; // Resource Token - public resourceTokenDatabaseId: ko.Observable; - public resourceTokenCollectionId: ko.Observable; public resourceTokenCollection: ko.Observable; - public resourceTokenPartitionKey: ko.Observable; - public isResourceTokenCollectionNodeSelected: ko.Computed; public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; // Tabs @@ -119,16 +95,12 @@ export default class Explorer { public tabsManager: TabsManager; public gitHubOAuthService: GitHubOAuthService; - - // features - public isHostedDataExplorerEnabled: ko.Computed; public isSchemaEnabled: ko.Computed; // Notebooks public isNotebookEnabled: ko.Observable; public isNotebooksEnabledForAccount: ko.Observable; public notebookServerInfo: ko.Observable; - public notebookWorkspaceManager: NotebookWorkspaceManager; public sparkClusterConnectionInfo: ko.Observable; public isSynapseLinkUpdating: ko.Observable; public memoryUsageInfo: ko.Observable; @@ -146,9 +118,6 @@ export default class Explorer { private static readonly MaxNbDatabasesToAutoExpand = 5; constructor(params?: ExplorerParams) { - this.setNotificationConsoleData = params?.setNotificationConsoleData; - this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted; - const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); @@ -163,8 +132,6 @@ export default class Explorer { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); - RouteHandler.getInstance().initHandler(); - this.notebookWorkspaceManager = new NotebookWorkspaceManager(); await this._refreshNotebooksEnabledStateForAccount(); this.isNotebookEnabled( userContext.authType !== AuthType.ResourceToken && @@ -190,56 +157,13 @@ export default class Explorer { this.memoryUsageInfo = ko.observable(); this.queriesClient = new QueriesClient(this); - - this.resourceTokenDatabaseId = ko.observable(); - this.resourceTokenCollectionId = ko.observable(); this.resourceTokenCollection = ko.observable(); - this.resourceTokenPartitionKey = ko.observable(); this.isSchemaEnabled = ko.computed(() => userContext.features.enableSchema); - this.databases = ko.observableArray(); - this.canSaveQueries = ko.computed(() => { - const savedQueriesDatabase: ViewModels.Database = _.find( - this.databases(), - (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName - ); - if (!savedQueriesDatabase) { - return false; - } - const savedQueriesCollection: ViewModels.Collection = - savedQueriesDatabase && - _.find( - savedQueriesDatabase.collections(), - (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName - ); - if (!savedQueriesCollection) { - return false; - } - return true; - }); - this.selectedNode = ko.observable(); - this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { + useSelectedNode.subscribe(() => { // Make sure switching tabs restores tabs display this.isTabsContentExpanded(false); }); - this.isResourceTokenCollectionNodeSelected = ko.computed(() => { - return ( - this.selectedNode() && - this.resourceTokenCollection() && - this.selectedNode().id() === this.resourceTokenCollection().id() - ); - }); - - const splitterBounds: SplitterBounds = { - min: ExplorerMetrics.SplitterMinWidth, - max: ExplorerMetrics.SplitterMaxWidth, - }; - this.splitter = new Splitter({ - splitterId: "h_splitter1", - leftId: "resourcetree", - bounds: splitterBounds, - direction: SplitterDirection.Vertical, - }); this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { if (userContext.features.enableFixedCollectionWithSharedThroughput) { @@ -253,44 +177,10 @@ export default class Explorer { return isCapabilityEnabled("EnableMongo"); }); - this.isServerlessEnabled = ko.computed( - () => - userContext.databaseAccount?.properties?.capabilities?.find( - (item) => item.name === Constants.CapabilityNames.EnableServerless - ) !== undefined - ); - - this.isHostedDataExplorerEnabled = ko.computed( - () => - configContext.platform === Platform.Portal && - !this.isRunningOnNationalCloud() && - userContext.apiType !== "Gremlin" - ); - this.selectedDatabaseId = ko.computed(() => { - const selectedNode = this.selectedNode(); - if (!selectedNode) { - return ""; - } - - switch (selectedNode.nodeKind) { - case "Collection": - return (selectedNode as ViewModels.CollectionBase).databaseId || ""; - case "Database": - return selectedNode.id() || ""; - case "DocumentId": - case "StoredProcedure": - case "Trigger": - case "UserDefinedFunction": - return selectedNode.collection.databaseId || ""; - default: - return ""; - } - }); - this.tabsManager = params?.tabsManager ?? new TabsManager(); this.tabsManager.openedTabs.subscribe((tabs) => { if (tabs.length === 0) { - this.selectedNode(undefined); + useSelectedNode.getState().setSelectedNode(undefined); useCommandBar.getState().setContextButtons([]); } }); @@ -387,6 +277,7 @@ export default class Explorer { if (configContext.enableSchemaAnalyzer) { userContext.features.enableSchemaAnalyzer = true; } + this.isAccountReady(true); } public openEnableSynapseLinkDialog(): void { @@ -441,45 +332,17 @@ export default class Explorer { // TODO: return result } - public isDatabaseNodeOrNoneSelected(): boolean { - return this.isNoneSelected() || this.isDatabaseNodeSelected(); - } - - public isDatabaseNodeSelected(): boolean { - return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false; - } - - public isNodeKindSelected(nodeKind: string): boolean { - return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false; - } - - public isNoneSelected(): boolean { - return this.selectedNode() == null; - } - - public logConsoleData(consoleData: ConsoleData): void { - this.setNotificationConsoleData(consoleData); - } - - public deleteInProgressConsoleDataWithId(id: string): void { - this.setInProgressConsoleDataIdToBeDeleted(id); - } - - public refreshDatabaseForResourceToken(): Q.Promise { - const databaseId = this.resourceTokenDatabaseId(); - const collectionId = this.resourceTokenCollectionId(); + public refreshDatabaseForResourceToken(): Promise { + const databaseId = userContext.parsedResourceToken?.databaseId; + const collectionId = userContext.parsedResourceToken?.collectionId; if (!databaseId || !collectionId) { - return Q.reject(); + return Promise.reject(); } - const deferred: Q.Deferred = Q.defer(); - readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { + return readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection)); - this.selectedNode(this.resourceTokenCollection()); - deferred.resolve(); + useSelectedNode.getState().setSelectedNode(this.resourceTokenCollection()); }); - - return deferred.promise; } public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise { @@ -504,11 +367,9 @@ export default class Explorer { }, startKey ); - const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); const deltaDatabases = this.getDeltaDatabases(databases); this.addDatabasesToList(deltaDatabases.toAdd); this.deleteDatabasesFromList(deltaDatabases.toDelete); - this.selectedNode(currentlySelectedNode); this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then( () => { deferred.resolve(); @@ -597,37 +458,19 @@ export default class Explorer { this._isInitializingNotebooks = true; await this.ensureNotebookWorkspaceRunning(); - let connectionInfo: DataModels.NotebookWorkspaceConnectionInfo = { - authToken: undefined, - notebookServerEndpoint: undefined, - }; - try { - connectionInfo = await this.notebookWorkspaceManager.getNotebookConnectionInfoAsync( - databaseAccount.id, - "default" - ); - } catch (error) { - this._isInitializingNotebooks = false; - handleError( - error, - "initNotebooks/getNotebookConnectionInfoAsync", - `Failed to get notebook workspace connection info: ${getErrorMessage(error)}` - ); - throw error; - } finally { - // Overwrite with feature flags - if (userContext.features.notebookServerUrl) { - connectionInfo.notebookServerEndpoint = userContext.features.notebookServerUrl; - } + const connectionInfo = await listConnectionInfo( + userContext.subscriptionId, + userContext.resourceGroup, + databaseAccount.name, + "default" + ); - if (userContext.features.notebookServerToken) { - connectionInfo.authToken = userContext.features.notebookServerToken; - } - - this.notebookServerInfo(connectionInfo); - this.notebookServerInfo.valueHasMutated(); - this.refreshNotebookList(); - } + this.notebookServerInfo({ + notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, + authToken: userContext.features.notebookServerToken || connectionInfo.authToken, + }); + this.notebookServerInfo.valueHasMutated(); + this.refreshNotebookList(); this._isInitializingNotebooks = false; } @@ -659,7 +502,11 @@ export default class Explorer { } try { - const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id); + const { value: workspaces } = await listByDatabaseAccount( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name + ); return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); } catch (error) { Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); @@ -674,8 +521,10 @@ export default class Explorer { let clearMessage; try { - const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync( - userContext.databaseAccount.id, + const notebookWorkspace = await getWorkspace( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, "default" ); if ( @@ -685,7 +534,7 @@ export default class Explorer { notebookWorkspace.properties.status.toLowerCase() === "stopped" ) { clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace"); - await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(userContext.databaseAccount.id, "default"); + await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default"); } } catch (error) { handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace"); @@ -713,73 +562,6 @@ export default class Explorer { } }; - public findSelectedDatabase(): ViewModels.Database { - if (!this.selectedNode()) { - return null; - } - if (this.selectedNode().nodeKind === "Database") { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id()); - } - return this.findSelectedCollection().database; - } - - public findDatabaseWithId(databaseId: string): ViewModels.Database { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId); - } - - public isLastNonEmptyDatabase(): boolean { - if ( - this.isLastDatabase() && - this.databases()[0] && - this.databases()[0].collections && - this.databases()[0].collections().length > 0 - ) { - return true; - } - return false; - } - - public isLastDatabase(): boolean { - if (this.databases().length > 1) { - return false; - } - return true; - } - - public isSelectedDatabaseShared(): boolean { - const database = this.findSelectedDatabase(); - if (!!database) { - return database.offer && !!database.offer(); - } - - return false; - } - - public configure(inputs: ViewModels.DataExplorerInputsFrame): void { - if (inputs != null) { - // In development mode, save the iframe message from the portal in session storage. - // This allows webpack hot reload to funciton properly - if (process.env.NODE_ENV === "development") { - sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); - } - this.isAccountReady(true); - } - } - - public findSelectedCollection(): ViewModels.Collection { - return (this.selectedNode().nodeKind === "Collection" - ? this.selectedNode() - : this.selectedNode().collection) as ViewModels.Collection; - } - - public isRunningOnNationalCloud(): boolean { - return ( - userContext.portalEnv === "blackforest" || - userContext.portalEnv === "fairfax" || - userContext.portalEnv === "mooncake" - ); - } - private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise { // we reload collections for all databases so the resource tree reflects any collection-level changes // i.e addition of stored procedures, etc. @@ -787,10 +569,11 @@ export default class Explorer { let loadCollectionPromises: Q.Promise[] = []; // If the user has a lot of databases, only load expanded databases. + const databases = useDatabases.getState().databases; const databasesToLoad = - this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand - ? this.databases() - : this.databases().filter((db) => db.isDatabaseExpanded()); + databases.length <= Explorer.MaxNbDatabasesToAutoExpand + ? databases + : databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { dataExplorerArea: Constants.Areas.ResourceTree, @@ -835,37 +618,16 @@ export default class Explorer { } } - public findCollection(databaseId: string, collectionId: string): ViewModels.Collection { - const database: ViewModels.Database = this.databases().find( - (database: ViewModels.Database) => database.id() === databaseId - ); - return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId); - } - - public isLastCollection(): boolean { - let collectionCount = 0; - if (this.databases().length == 0) { - return false; - } - for (let i = 0; i < this.databases().length; i++) { - const database = this.databases()[i]; - collectionCount += database.collections().length; - if (collectionCount > 1) { - return false; - } - } - return true; - } - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[] ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[]; } { + const databases = useDatabases.getState().databases; const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const databaseExists = _.some( - this.databases(), + databases, (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id ); return !databaseExists; @@ -875,7 +637,7 @@ export default class Explorer { ); let databasesToDelete: ViewModels.Database[] = []; - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { + ko.utils.arrayForEach(databases, (database: ViewModels.Database) => { const databasePresentInUpdatedList = _.some( updatedDatabaseList, (db: DataModels.Database) => db.id === database.id() @@ -889,24 +651,12 @@ export default class Explorer { } private addDatabasesToList(databases: ViewModels.Database[]): void { - this.databases( - this.databases() - .concat(databases) - .sort((database1, database2) => database1.id().localeCompare(database2.id())) - ); + useDatabases.getState().addDatabases(databases); } private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { - const databasesToKeep: ViewModels.Database[] = []; - - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { - const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id); - if (!shouldRemoveDatabase) { - databasesToKeep.push(database); - } - }); - - this.databases(databasesToKeep); + const deleteDatabase = useDatabases.getState().deleteDatabase; + databasesToRemove.forEach((database) => deleteDatabase(database)); } public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { @@ -1068,7 +818,6 @@ export default class Explorer { tabPath: notebookContentItem.path, collection: null, masterKey: userContext.masterKey || "", - hashLocation: "notebooks", isTabsContentExpanded: ko.observable(true), onLoadStartKey: null, container: this, @@ -1368,33 +1117,34 @@ export default class Explorer { public openNotebookTerminal(kind: ViewModels.TerminalKind) { let title: string; - let hashLocation: string; switch (kind) { case ViewModels.TerminalKind.Default: title = "Terminal"; - hashLocation = "terminal"; break; case ViewModels.TerminalKind.Mongo: title = "Mongo Shell"; - hashLocation = "mongo-shell"; break; case ViewModels.TerminalKind.Cassandra: title = "Cassandra Shell"; - hashLocation = "cassandra-shell"; break; default: throw new Error("Terminal kind: ${kind} not supported"); } - const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => - tab.hashLocation().startsWith(hashLocation) + const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.Terminal, + (tab) => tab.tabTitle() === title ) as TerminalTab[]; - const index = terminalTabs.length + 1; + let index = 1; + if (terminalTabs.length > 0) { + index = terminalTabs[terminalTabs.length - 1].index + 1; + } + const newTab = new TerminalTab({ account: userContext.databaseAccount, tabKind: ViewModels.CollectionTabKind.Terminal, @@ -1402,11 +1152,11 @@ export default class Explorer { title: `${title} ${index}`, tabPath: `${title} ${index}`, collection: null, - hashLocation: `${hashLocation} ${index}`, isTabsContentExpanded: ko.observable(true), onLoadStartKey: null, container: this, kind: kind, + index: index, }); this.tabsManager.activateNewTab(newTab); @@ -1419,11 +1169,10 @@ export default class Explorer { isFavorite?: boolean ) { const title = "Gallery"; - const hashLocation = "gallery"; const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default; const galleryTab = this.tabsManager .getTabs(ViewModels.CollectionTabKind.Gallery) - .find((tab) => tab.hashLocation() == hashLocation); + .find((tab) => tab.tabTitle() == title); if (galleryTab instanceof GalleryTab) { this.tabsManager.activateTab(galleryTab); @@ -1432,9 +1181,8 @@ export default class Explorer { new GalleryTab( { tabKind: ViewModels.CollectionTabKind.Gallery, - title: title, + title, tabPath: title, - hashLocation: hashLocation, onLoadStartKey: null, isTabsContentExpanded: ko.observable(true), }, @@ -1452,11 +1200,19 @@ export default class Explorer { } } - public onNewCollectionClicked(databaseId?: string): void { + public async onNewCollectionClicked(databaseId?: string): Promise { if (userContext.apiType === "Cassandra") { - this.openCassandraAddCollectionPane(); + useSidePanel + .getState() + .openSidePanel( + "Add Table", + + ); } else { - this.openAddCollectionPanel(databaseId); + await useDatabases.getState().loadDatabaseOffers(); + useSidePanel + .getState() + .openSidePanel("New " + getCollectionName(), ); } } @@ -1500,50 +1256,8 @@ export default class Explorer { } } - public async loadDatabaseOffers(): Promise { - await Promise.all( - this.databases()?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - }) - ); - } - - public isFirstResourceCreated(): boolean { - const databases: ViewModels.Database[] = this.databases(); - - if (!databases || databases.length === 0) { - return false; - } - - return databases.some((database) => { - // user has created at least one collection - if (database.collections()?.length > 0) { - return true; - } - // user has created a database with shared throughput - if (database.offer()) { - return true; - } - // use has created an empty database without shared throughput - return false; - }); - } - public openDeleteCollectionConfirmationPane(): void { - useSidePanel - .getState() - .openSidePanel("Delete " + getCollectionName(), ); - } - - public openDeleteDatabaseConfirmationPane(): void { - useSidePanel - .getState() - .openSidePanel( - "Delete " + getDatabaseName(), - - ); - } public openUploadItemsPanePane(): void { - useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); + useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { useSidePanel @@ -1551,12 +1265,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public async openAddCollectionPanel(databaseId?: string): Promise { - await this.loadDatabaseOffers(); - useSidePanel - .getState() - .openSidePanel("New " + getCollectionName(), ); - } public openAddDatabasePane(): void { useSidePanel.getState().openSidePanel("New " + getDatabaseName(), ); } @@ -1579,14 +1287,6 @@ export default class Explorer { ); } - public openCassandraAddCollectionPane(): void { - useSidePanel - .getState() - .openSidePanel( - "Add Table", - - ); - } public openGitHubReposPanel(header: string, junoClient?: JunoClient): void { useSidePanel .getState() @@ -1600,43 +1300,9 @@ export default class Explorer { ); } - public openAddTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void { - useSidePanel - .getState() - .openSidePanel( - "Add Table Entity", - - ); - } public openSetupNotebooksPanel(title: string, description: string): void { useSidePanel .getState() .openSidePanel(title, ); } - - public openEditTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void { - useSidePanel - .getState() - .openSidePanel( - "Edit Table Entity", - - ); - } - - public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void { - useSidePanel.getState().openSidePanel("Select Column", ); - } - public openSettingPane(): void { - useSidePanel.getState().openSidePanel("Settings", ); - } } diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx index 1954186c8..6ce0a11bf 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx @@ -10,7 +10,7 @@ import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPag import * as DataModels from "../../../Contracts/DataModels"; import * as StorageUtility from "../../../Shared/StorageUtility"; import { TabComponent } from "../../Controls/Tabs/TabComponent"; -import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; +import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData"; import GraphTab from "../../Tabs/GraphTab"; import * as D3ForceGraph from "./D3ForceGraph"; import { GraphData } from "./GraphData"; diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx index 5b2441e40..cf2fbbb3a 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx @@ -18,7 +18,7 @@ import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Ut import { EditorReact } from "../../Controls/Editor/EditorReact"; import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; import * as TabComponent from "../../Controls/Tabs/TabComponent"; -import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; +import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData"; import { IGraphConfig } from "../../Tabs/GraphTab"; import { ArraysByKeyCache } from "./ArraysByKeyCache"; import * as D3ForceGraph from "./D3ForceGraph"; diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx deleted file mode 100644 index 984419398..000000000 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorerAdapter.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { IGraphConfig } from "../../Tabs/GraphTab"; -import { GraphAccessor, GraphExplorer } from "./GraphExplorer"; -interface Parameter { - onIsNewVertexDisabledChange: (isEnabled: boolean) => void; - onGraphAccessorCreated: (instance: GraphAccessor) => void; - onIsFilterQueryLoading: (isFilterQueryLoading: boolean) => void; - onIsValidQuery: (isValidQuery: boolean) => void; - onIsPropertyEditing: (isEditing: boolean) => void; - onIsGraphDisplayed: (isDisplayed: boolean) => void; - onResetDefaultGraphConfigValues: () => void; - - collectionPartitionKeyProperty: string; - graphBackendEndpoint: string; - databaseId: string; - collectionId: string; - masterKey: string; - - onLoadStartKey: number; - onLoadStartKeyChange: (newKey: number) => void; - resourceId: string; - - igraphConfigUiData: ViewModels.IGraphConfigUiData; - igraphConfig: IGraphConfig; - setIConfigUiData?: (data: string[]) => void; -} - -interface IGraphExplorerProps { - isChanged: boolean; -} - -interface IGraphExplorerStates { - isChangedState: boolean; -} - -export interface GraphExplorerAdapter - extends ReactAdapter, - React.Component {} -export class GraphExplorerAdapter implements ReactAdapter { - public params: Parameter; - public parameters = {}; - public isNewVertexDisabled: boolean; - - public constructor(params: Parameter, props?: IGraphExplorerProps) { - this.params = params; - } - - public renderComponent(): JSX.Element { - return ( - - ); - } -} diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.test.ts b/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.test.ts index 754f6217c..b661880a7 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.test.ts @@ -1,9 +1,7 @@ -import * as GraphUtil from "./GraphUtil"; -import { GraphData, GremlinVertex, GremlinEdge } from "./GraphData"; import * as sinon from "sinon"; +import { GraphData, GremlinEdge, GremlinVertex } from "./GraphData"; import { GraphExplorer } from "./GraphExplorer"; -window.$ = window.jQuery = require("jquery"); - +import * as GraphUtil from "./GraphUtil"; const OUT_E_MATCHER = "g\\.V\\(.*\\).outE\\(\\).*\\.as\\('e'\\).inV\\(\\)\\.as\\('v'\\)\\.select\\('e', *'v'\\)"; const IN_E_MATCHER = "g\\.V\\(.*\\).inE\\(\\).*\\.as\\('e'\\).outV\\(\\)\\.as\\('v'\\)\\.select\\('e', *'v'\\)"; diff --git a/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx b/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx index e308d7ece..3314bf9eb 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx @@ -5,21 +5,26 @@ */ import * as React from "react"; -import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer"; -import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel"; -import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent"; -import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent"; -import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent"; -import * as ViewModels from "../../../Contracts/ViewModels"; -import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent"; -import * as EditorNeighbors from "./EditorNeighborsComponent"; -import EditIcon from "../../../../images/edit.svg"; -import DeleteIcon from "../../../../images/delete.svg"; -import CheckIcon from "../../../../images/check.svg"; import CancelIcon from "../../../../images/cancel.svg"; -import { GraphExplorer } from "./GraphExplorer"; -import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; +import CheckIcon from "../../../../images/check.svg"; +import DeleteIcon from "../../../../images/delete.svg"; +import EditIcon from "../../../../images/edit.svg"; +import * as ViewModels from "../../../Contracts/ViewModels"; import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement"; +import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel"; +import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent"; +import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData"; +import * as EditorNeighbors from "./EditorNeighborsComponent"; +import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent"; +import { + EditedEdges, + EditedProperties, + GraphExplorer, + GraphHighlightedNodeData, + PossibleVertex, +} from "./GraphExplorer"; +import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent"; +import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent"; export enum Mode { READONLY_PROP, diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index efaa5a3dd..03a3c1ef8 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -8,9 +8,9 @@ import * as React from "react"; import create, { UseStore } from "zustand"; import { StyleConstants } from "../../../Common/Constants"; import * as ViewModels from "../../../Contracts/ViewModels"; -import { useObservable } from "../../../hooks/useObservable"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; +import { useSelectedNode } from "../../useSelectedNode"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarUtil from "./CommandBarUtil"; @@ -29,13 +29,13 @@ export const useCommandBar: UseStore = create((set) => ({ })); export const CommandBar: React.FC = ({ container }: Props) => { - useObservable(container.selectedNode); + const selectedNodeState = useSelectedNode(); const buttons = useCommandBar((state) => state.contextButtons); const backgroundColor = StyleConstants.BaseLight; - const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container); + const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState); const contextButtons = (buttons || []).concat( - CommandBarComponentButtonFactory.createContextCommandBarButtons(container) + CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState) ); const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index 5af290993..3e8ae6b04 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -1,17 +1,22 @@ import * as ko from "knockout"; import { AuthType } from "../../../AuthType"; import { DatabaseAccount } from "../../../Contracts/DataModels"; +import { CollectionBase } from "../../../Contracts/ViewModels"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { updateUserContext } from "../../../UserContext"; import Explorer from "../../Explorer"; import NotebookManager from "../../Notebook/NotebookManager"; +import { useSelectedNode } from "../../useSelectedNode"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; describe("CommandBarComponentButtonFactory tests", () => { let mockExplorer: Explorer; + afterEach(() => useSelectedNode.getState().setSelectedNode(undefined)); + describe("Enable Azure Synapse Link Button", () => { const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link"; + const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { mockExplorer = {} as Explorer; @@ -23,17 +28,12 @@ describe("CommandBarComponentButtonFactory tests", () => { } as DatabaseAccount, }); mockExplorer.isSynapseLinkUpdating = ko.observable(false); - - mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); - mockExplorer.isRunningOnNationalCloud = () => false; }); it("Account is not serverless - button should be visible", () => { - mockExplorer.isServerlessEnabled = ko.computed(() => false); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const enableAzureSynapseLinkBtn = buttons.find( (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel ); @@ -41,9 +41,14 @@ describe("CommandBarComponentButtonFactory tests", () => { }); it("Account is serverless - button should be hidden", () => { - mockExplorer.isServerlessEnabled = ko.computed(() => true); - - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableServerless" }], + }, + } as DatabaseAccount, + }); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const enableAzureSynapseLinkBtn = buttons.find( (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel ); @@ -53,10 +58,12 @@ describe("CommandBarComponentButtonFactory tests", () => { describe("Enable notebook button", () => { const enableNotebookBtnLabel = "Enable Notebooks (Preview)"; + const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { mockExplorer = {} as Explorer; updateUserContext({ + portalEnv: "prod", databaseAccount: { properties: { capabilities: [{ name: "EnableTable" }], @@ -64,18 +71,19 @@ describe("CommandBarComponentButtonFactory tests", () => { } as DatabaseAccount, }); mockExplorer.isSynapseLinkUpdating = ko.observable(false); - mockExplorer.isSynapseLinkUpdating = ko.observable(false); + }); - mockExplorer.isDatabaseNodeOrNoneSelected = () => true; - mockExplorer.isServerlessEnabled = ko.computed(() => false); + afterEach(() => { + updateUserContext({ + portalEnv: "prod", + }); }); it("Notebooks is already enabled - button should be hidden", () => { mockExplorer.isNotebookEnabled = ko.observable(true); mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); - mockExplorer.isRunningOnNationalCloud = ko.observable(false); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); expect(enableNotebookBtn).toBeUndefined(); }); @@ -83,9 +91,11 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Account is running on one of the national clouds - button should be hidden", () => { mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); - mockExplorer.isRunningOnNationalCloud = ko.observable(true); + updateUserContext({ + portalEnv: "mooncake", + }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); expect(enableNotebookBtn).toBeUndefined(); }); @@ -93,9 +103,8 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Notebooks is not enabled but is available - button should be shown and enabled", () => { mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); - mockExplorer.isRunningOnNationalCloud = ko.observable(false); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); expect(enableNotebookBtn).toBeDefined(); expect(enableNotebookBtn.disabled).toBe(false); @@ -105,9 +114,8 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); - mockExplorer.isRunningOnNationalCloud = ko.observable(false); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel); expect(enableNotebookBtn).toBeDefined(); expect(enableNotebookBtn.disabled).toBe(true); @@ -119,6 +127,7 @@ describe("CommandBarComponentButtonFactory tests", () => { describe("Open Mongo Shell button", () => { const openMongoShellBtnLabel = "Open Mongo Shell"; + const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { mockExplorer = {} as Explorer; @@ -130,9 +139,6 @@ describe("CommandBarComponentButtonFactory tests", () => { } as DatabaseAccount, }); mockExplorer.isSynapseLinkUpdating = ko.observable(false); - - mockExplorer.isDatabaseNodeOrNoneSelected = () => true; - mockExplorer.isServerlessEnabled = ko.computed(() => false); mockExplorer.isShellEnabled = ko.observable(true); }); @@ -148,7 +154,7 @@ describe("CommandBarComponentButtonFactory tests", () => { }); mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); - mockExplorer.isRunningOnNationalCloud = ko.observable(false); + mockExplorer.isShellEnabled = ko.observable(true); }); @@ -156,21 +162,23 @@ describe("CommandBarComponentButtonFactory tests", () => { updateUserContext({ apiType: "SQL", }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); expect(openMongoShellBtn).toBeUndefined(); }); it("Running on a national cloud - button should be hidden", () => { - mockExplorer.isRunningOnNationalCloud = ko.observable(true); + updateUserContext({ + portalEnv: "mooncake", + }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); expect(openMongoShellBtn).toBeUndefined(); }); it("Notebooks is not enabled and is unavailable - button should be hidden", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); expect(openMongoShellBtn).toBeUndefined(); }); @@ -178,7 +186,7 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Notebooks is not enabled and is available - button should be hidden", () => { mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); expect(openMongoShellBtn).toBeUndefined(); }); @@ -186,7 +194,7 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { mockExplorer.isNotebookEnabled = ko.observable(true); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn.disabled).toBe(false); @@ -197,7 +205,7 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.isNotebookEnabled = ko.observable(true); mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn.disabled).toBe(false); @@ -209,7 +217,7 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); mockExplorer.isShellEnabled = ko.observable(false); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); expect(openMongoShellBtn).toBeUndefined(); }); @@ -217,6 +225,7 @@ describe("CommandBarComponentButtonFactory tests", () => { describe("Open Cassandra Shell button", () => { const openCassandraShellBtnLabel = "Open Cassandra Shell"; + const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { mockExplorer = {} as Explorer; @@ -228,9 +237,6 @@ describe("CommandBarComponentButtonFactory tests", () => { } as DatabaseAccount, }); mockExplorer.isSynapseLinkUpdating = ko.observable(false); - - mockExplorer.isDatabaseNodeOrNoneSelected = () => true; - mockExplorer.isServerlessEnabled = ko.computed(() => false); }); beforeEach(() => { @@ -243,7 +249,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }); mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); - mockExplorer.isRunningOnNationalCloud = ko.observable(false); }); it("Cassandra Api not available - button should be hidden", () => { @@ -255,21 +260,23 @@ describe("CommandBarComponentButtonFactory tests", () => { } as DatabaseAccount, }); console.log(mockExplorer); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); it("Running on a national cloud - button should be hidden", () => { - mockExplorer.isRunningOnNationalCloud = ko.observable(true); + updateUserContext({ + portalEnv: "mooncake", + }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); @@ -277,7 +284,7 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Notebooks is not enabled and is available - button should be shown and enabled", () => { mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); @@ -285,7 +292,7 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { mockExplorer.isNotebookEnabled = ko.observable(true); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn.disabled).toBe(false); @@ -296,7 +303,7 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.isNotebookEnabled = ko.observable(true); mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn.disabled).toBe(false); @@ -307,6 +314,7 @@ describe("CommandBarComponentButtonFactory tests", () => { describe("GitHub buttons", () => { const connectToGitHubBtnLabel = "Connect to GitHub"; const manageGitHubSettingsBtnLabel = "Manage GitHub settings"; + const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { mockExplorer = {} as Explorer; @@ -319,12 +327,10 @@ describe("CommandBarComponentButtonFactory tests", () => { }); mockExplorer.isSynapseLinkUpdating = ko.observable(false); - mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); - mockExplorer.isRunningOnNationalCloud = ko.observable(false); + mockExplorer.notebookManager = new NotebookManager(); mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined); - mockExplorer.isServerlessEnabled = ko.computed(() => false); }); beforeEach(() => { @@ -338,7 +344,7 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => { mockExplorer.isNotebookEnabled = ko.observable(true); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel); expect(connectToGitHubBtn).toBeDefined(); }); @@ -347,7 +353,7 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.isNotebookEnabled = ko.observable(true); mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const manageGitHubSettingsBtn = buttons.find( (button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel ); @@ -355,7 +361,7 @@ describe("CommandBarComponentButtonFactory tests", () => { }); it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel); expect(connectToGitHubBtn).toBeUndefined(); @@ -368,11 +374,13 @@ describe("CommandBarComponentButtonFactory tests", () => { }); describe("Resource token", () => { + const mockCollection = { id: ko.observable("test") } as CollectionBase; + useSelectedNode.getState().setSelectedNode(mockCollection); + const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { mockExplorer = {} as Explorer; - mockExplorer.isDatabaseNodeOrNoneSelected = () => true; - mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true); - mockExplorer.isServerlessEnabled = ko.computed(() => false); + mockExplorer.resourceTokenCollection = ko.observable(mockCollection); + updateUserContext({ authType: AuthType.ResourceToken, }); @@ -384,7 +392,7 @@ describe("CommandBarComponentButtonFactory tests", () => { kind: "DocumentDB", } as DatabaseAccount, }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); expect(buttons.length).toBe(2); expect(buttons[0].commandButtonLabel).toBe("New SQL Query"); expect(buttons[0].disabled).toBe(false); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 7b185fdba..35f2ee82d 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -24,16 +24,23 @@ import * as ViewModels from "../../../Contracts/ViewModels"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { userContext } from "../../../UserContext"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; +import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; +import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; import { OpenFullScreen } from "../../OpenFullScreen"; import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane"; +import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane"; +import { SelectedNodeState } from "../../useSelectedNode"; let counter = 0; -export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { +export function createStaticCommandBarButtons( + container: Explorer, + selectedNodeState: SelectedNodeState +): CommandButtonComponentProps[] { if (userContext.authType === AuthType.ResourceToken) { - return createStaticCommandBarButtonsForResourceToken(container); + return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState); } const newCollectionBtn = createNewCollectionGroup(container); @@ -68,7 +75,9 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto buttons.push(createNotebookWorkspaceResetButton(container)); if ( - (userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) || + (userContext.apiType === "Mongo" && + container.isShellEnabled() && + selectedNodeState.isDatabaseNodeOrNoneSelected()) || userContext.apiType === "Cassandra" ) { buttons.push(createDivider()); @@ -79,23 +88,23 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto } } } else { - if (!container.isRunningOnNationalCloud()) { + if (!isRunningOnNationalCloud()) { buttons.push(createEnableNotebooksButton(container)); } } - if (!container.isDatabaseNodeOrNoneSelected()) { + if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) { const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; if (isQuerySupported) { buttons.push(createDivider()); - const newSqlQueryBtn = createNewSQLQueryButton(container); + const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState); buttons.push(newSqlQueryBtn); } - if (isQuerySupported && container.selectedNode() && container.findSelectedCollection()) { + if (isQuerySupported && selectedNodeState.findSelectedCollection()) { const openQueryBtn = createOpenQueryButton(container); - openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)]; + openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; buttons.push(openQueryBtn); } @@ -105,16 +114,16 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto iconSrc: AddStoredProcedureIcon, iconAlt: label, onCommandClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); + const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); }, commandButtonLabel: label, ariaLabel: label, hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected(), + disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(), }; - newStoredProcedureBtn.children = createScriptCommandButtons(container); + newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState); buttons.push(newStoredProcedureBtn); } } @@ -122,16 +131,19 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto return buttons; } -export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { +export function createContextCommandBarButtons( + container: Explorer, + selectedNodeState: SelectedNodeState +): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = []; - if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { + if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell"; const newMongoShellBtn: CommandButtonComponentProps = { iconSrc: HostedTerminalIcon, iconAlt: label, onCommandClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); + const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); if (container.isShellEnabled()) { container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); } else { @@ -141,7 +153,7 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt commandButtonLabel: label, ariaLabel: label, hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo", + disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo", }; buttons.push(newMongoShellBtn); } @@ -154,7 +166,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt { iconSrc: SettingsIcon, iconAlt: "Settings", - onCommandClick: container.openSettingPane, + onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", ), commandButtonLabel: undefined, ariaLabel: "Settings", tooltipText: "Settings", @@ -163,7 +175,10 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt }, ]; - if (container.isHostedDataExplorerEnabled()) { + const showOpenFullScreen = + configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin"; + + if (showOpenFullScreen) { const label = "Open Full Screen"; const fullScreenButton: CommandButtonComponentProps = { iconSrc: OpenInTabIcon, @@ -175,7 +190,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt ariaLabel: label, tooltipText: label, hasPopup: false, - disabled: !container.isHostedDataExplorerEnabled(), + disabled: !showOpenFullScreen, className: "OpenFullScreen", }; buttons.push(fullScreenButton); @@ -234,7 +249,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo return undefined; } - if (container.isServerlessEnabled()) { + if (isServerlessAccount()) { return undefined; } @@ -273,20 +288,20 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps { }; } -function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps { +function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandButtonComponentProps { if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") { const label = "New SQL Query"; return { iconSrc: AddSqlQueryIcon, iconAlt: label, onCommandClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); + const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); }, commandButtonLabel: label, ariaLabel: label, hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected(), + disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(), }; } else if (userContext.apiType === "Mongo") { const label = "New Query"; @@ -294,23 +309,24 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro iconSrc: AddSqlQueryIcon, iconAlt: label, onCommandClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); + const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); }, commandButtonLabel: label, ariaLabel: label, hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected(), + disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(), }; } return undefined; } -export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] { +export function createScriptCommandButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = []; - const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported(); + const shouldEnableScriptsCommands: boolean = + !selectedNodeState.isDatabaseNodeOrNoneSelected() && areScriptsSupported(); if (shouldEnableScriptsCommands) { const label = "New Stored Procedure"; @@ -318,13 +334,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo iconSrc: AddStoredProcedureIcon, iconAlt: label, onCommandClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); + const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); }, commandButtonLabel: label, ariaLabel: label, hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected(), + disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(), }; buttons.push(newStoredProcedureBtn); } @@ -335,13 +351,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo iconSrc: AddUdfIcon, iconAlt: label, onCommandClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); + const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); }, commandButtonLabel: label, ariaLabel: label, hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected(), + disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(), }; buttons.push(newUserDefinedFunctionBtn); } @@ -352,13 +368,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo iconSrc: AddTriggerIcon, iconAlt: label, onCommandClick: () => { - const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); + const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection); }, commandButtonLabel: label, ariaLabel: label, hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected(), + disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(), }; buttons.push(newTriggerBtn); } @@ -405,12 +421,12 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps }; } -function createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps { +function createOpenQueryFromDiskButton(): CommandButtonComponentProps { const label = "Open Query From Disk"; return { iconSrc: OpenQueryFromDiskIcon, iconAlt: label, - onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", ), + onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", ), commandButtonLabel: label, ariaLabel: label, hasPopup: true, @@ -531,19 +547,25 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp }; } -function createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] { - const newSqlQueryBtn = createNewSQLQueryButton(container); +function createStaticCommandBarButtonsForResourceToken( + container: Explorer, + selectedNodeState: SelectedNodeState +): CommandButtonComponentProps[] { + const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState); const openQueryBtn = createOpenQueryButton(container); - newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected(); + const isResourceTokenCollectionNodeSelected: boolean = + container.resourceTokenCollection() && + container.resourceTokenCollection().id() === selectedNodeState.selectedNode?.id(); + newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected; newSqlQueryBtn.onCommandClick = () => { const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection(); resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined); }; - openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected(); + openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected; if (!openQueryBtn.disabled) { - openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)]; + openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; } return [newSqlQueryBtn, openQueryBtn]; diff --git a/src/Explorer/Menus/NavBar/ControlBarComponent.tsx b/src/Explorer/Menus/NavBar/ControlBarComponent.tsx deleted file mode 100644 index 26864cea5..000000000 --- a/src/Explorer/Menus/NavBar/ControlBarComponent.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * React component for control bar - */ - -import * as React from "react"; -import { - CommandButtonComponent, - CommandButtonComponentProps, -} from "../../Controls/CommandButton/CommandButtonComponent"; - -export interface ControlBarComponentProps { - buttons: CommandButtonComponentProps[]; -} - -export class ControlBarComponent extends React.Component { - private static renderButtons(commandButtonOptions: CommandButtonComponentProps[]): JSX.Element[] { - return commandButtonOptions.map( - (btn: CommandButtonComponentProps, index: number): JSX.Element => { - // Remove label - btn.commandButtonLabel = undefined; - return CommandButtonComponent.renderButton(btn, `${index}`); - } - ); - } - - public render(): JSX.Element { - if (!this.props.buttons || this.props.buttons.length < 1) { - return ; - } - - return {ControlBarComponent.renderButtons(this.props.buttons)}; - } -} diff --git a/src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx b/src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx deleted file mode 100644 index b6af77dbe..000000000 --- a/src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * This adapter is responsible to render the React component - * If the component signals a change through the callback passed in the properties, it must render the React component when appropriate - * and update any knockout observables passed from the parent. - */ - -import * as ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { ControlBarComponent } from "./ControlBarComponent"; -import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; - -export class ControlBarComponentAdapter implements ReactAdapter { - public parameters: ko.Observable; - - constructor(private buttons: ko.ObservableArray) { - this.buttons.subscribe(() => this.forceRender()); - this.parameters = ko.observable(Date.now()); - } - - public renderComponent(): JSX.Element { - return ; - } - - public forceRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} diff --git a/src/Explorer/Menus/NotificationConsole/ConsoleData.tsx b/src/Explorer/Menus/NotificationConsole/ConsoleData.tsx new file mode 100644 index 000000000..2ede2f003 --- /dev/null +++ b/src/Explorer/Menus/NotificationConsole/ConsoleData.tsx @@ -0,0 +1,16 @@ +/** + * Interface for the data/content that will be recorded + */ + +export interface ConsoleData { + type: ConsoleDataType; + date: string; + message: string; + id?: string; +} + +export enum ConsoleDataType { + Info = 0, + Error = 1, + InProgress = 2, +} diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx index 05df0f46f..711e0db35 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx @@ -1,10 +1,7 @@ import { shallow } from "enzyme"; import React from "react"; -import { - ConsoleDataType, - NotificationConsoleComponent, - NotificationConsoleComponentProps, -} from "./NotificationConsoleComponent"; +import { ConsoleDataType } from "./ConsoleData"; +import { NotificationConsoleComponent, NotificationConsoleComponentProps } from "./NotificationConsoleComponent"; describe("NotificationConsoleComponent", () => { const createBlankProps = (): NotificationConsoleComponentProps => { diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index 218bd4893..c715159ca 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -17,25 +17,7 @@ import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; import { userContext } from "../../../UserContext"; - -/** - * Log levels - */ -export enum ConsoleDataType { - Info = 0, - Error = 1, - InProgress = 2, -} - -/** - * Interface for the data/content that will be recorded - */ -export interface ConsoleData { - type: ConsoleDataType; - date: string; - message: string; - id?: string; -} +import { ConsoleData, ConsoleDataType } from "./ConsoleData"; export interface NotificationConsoleComponentProps { isConsoleExpanded: boolean; @@ -323,14 +305,13 @@ const PrPreview = (props: { pr: string }) => { ); }; -export const NotificationConsole: React.FC< - Pick -> = ({ - consoleData, - inProgressConsoleDataIdToBeDeleted, -}: Pick) => { +export const NotificationConsole: React.FC = () => { const setIsExpanded = useNotificationConsole((state) => state.setIsExpanded); const isExpanded = useNotificationConsole((state) => state.isExpanded); + const consoleData = useNotificationConsole((state) => state.consoleData); + const inProgressConsoleDataIdToBeDeleted = useNotificationConsole( + (state) => state.inProgressConsoleDataIdToBeDeleted + ); // TODO Refactor NotificationConsoleComponent into a functional component and remove this wrapper // This component only exists so we can use hooks and pass them down to a non-functional component return ( diff --git a/src/Explorer/Notebook/NotebookClientV2.ts b/src/Explorer/Notebook/NotebookClientV2.ts index e7c1fa289..86318a166 100644 --- a/src/Explorer/Notebook/NotebookClientV2.ts +++ b/src/Explorer/Notebook/NotebookClientV2.ts @@ -21,7 +21,7 @@ import { makeStateRecord, makeTransformsRecord, } from "@nteract/core"; -import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration"; +import { configOption, defineConfigOption } from "@nteract/mythic-configuration"; import { Media } from "@nteract/outputs"; import TransformVDOM from "@nteract/transform-vdom"; import * as Immutable from "immutable"; @@ -242,22 +242,27 @@ export class NotebookClientV2 { ); // Additional configuration - this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "monaco")); + this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror")); this.store.dispatch( configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs) ); - createConfigCollection({ - key: "monaco", - }); - defineConfigOption({ - label: "Show Line numbers", - key: "monaco.lineNumbers", - values: [ - { label: "Yes", value: true }, - { label: "No", value: false }, - ], - defaultValue: true, - }); + this.store.dispatch(configOption("codeMirror.lineNumbers").action(true)); + + const readOnlyConfigOption = configOption("codeMirror.readOnly"); + const readOnlyValue = params.isReadOnly ? "nocursor" : undefined; + if (!readOnlyConfigOption) { + defineConfigOption({ + label: "Read-only", + key: "codeMirror.readOnly", + values: [ + { label: "Read-Only", value: "nocursor" }, + { label: "Not read-only", value: undefined }, + ], + defaultValue: readOnlyValue, + }); + } else { + this.store.dispatch(readOnlyConfigOption.action(readOnlyValue)); + } } /** diff --git a/src/Explorer/Notebook/NotebookComponent/NotebookComponent.less b/src/Explorer/Notebook/NotebookComponent/NotebookComponent.less index 36a819b88..1bd303e1d 100644 --- a/src/Explorer/Notebook/NotebookComponent/NotebookComponent.less +++ b/src/Explorer/Notebook/NotebookComponent/NotebookComponent.less @@ -1,10 +1,10 @@ .notebookComponentContainer { - text-transform:none; - line-height:1.28581; - letter-spacing:0; - font-size:14px; - font-weight:400; - color:#182026; + text-transform: none; + line-height: 1.28581; + letter-spacing: 0; + font-size: 14px; + font-weight: 400; + color: #182026; height: 100%; .hotKeys { diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts index 8b961450f..3764360ef 100644 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ b/src/Explorer/Notebook/NotebookContainerClient.ts @@ -6,6 +6,7 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import * as Logger from "../../Common/Logger"; import * as DataModels from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; +import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; export class NotebookContainerClient { @@ -130,16 +131,18 @@ export class NotebookContainerClient { } private async recreateNotebookWorkspaceAsync(): Promise { - const explorer = window.dataExplorer; const { databaseAccount } = userContext; if (!databaseAccount?.id) { throw new Error("DataExplorer not initialized"); } - - const notebookWorkspaceManager = explorer.notebookWorkspaceManager; try { - await notebookWorkspaceManager.deleteNotebookWorkspaceAsync(databaseAccount?.id, "default"); - await notebookWorkspaceManager.createNotebookWorkspaceAsync(databaseAccount?.id, "default"); + await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default"); + await createOrUpdate( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + "default" + ); } catch (error) { Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync"); return Promise.reject(error); diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.less b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.less index 541062c6f..d124cd0f2 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.less +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.less @@ -1,56 +1,68 @@ .NotebookReadOnlyRender { - .nteract-cell-container { - margin-bottom: 10px; - } + .nteract-cell-container { + margin-bottom: 10px; + } - .nteract-cell { - padding: 0.5px; - border: 1px solid #ffffff; - border-left: 3px solid #ffffff; - } + .nteract-cell { + padding: 0.5px; + border: 1px solid #ffffff; + border-left: 3px solid #ffffff; + } - .CodeMirror-scroll { - background-color: #f5f5f5; - } + .CodeMirror-scroll { + overflow: hidden !important; + } - .CodeMirror-lines { - cursor: default; - } + .CodeMirror-lines { + cursor: default; + } - .nteract-cell:hover { - border: 1px solid #0078d4; - border-left: 3px solid #0078d4; + .CodeMirror { + height: inherit; + } - .CodeMirror-scroll { - background-color: #ffffff; - } + .CodeMirror-scroll, + .CodeMirror-linenumber, + .CodeMirror-gutters { + background-color: #f5f5f5; + } - .nteract-cell-outputs { - border-top: 1px solid #d7d7d7; - } + .nteract-cell:hover { + border: 1px solid #0078d4; + border-left: 3px solid #0078d4; - .nteract-md-cell { - background-color: #ffffff; - } + .CodeMirror-scroll, + .CodeMirror-linenumber, + .CodeMirror-gutters { + background-color: #ffffff; } .nteract-cell-outputs { - padding: 10px; - border-top: 1px solid #ffffff; - - pre { - background-color: #ffffff; - border: none; - padding: 0px; - margin: 0px; - } + border-top: 1px solid #d7d7d7; } .nteract-md-cell { - background-color: #f5f5f5; + background-color: #ffffff; } + } - .nteract-cell:hover.nteract-md-cell { - background-color: #ffffff; + .nteract-cell-outputs { + padding: 10px; + border-top: 1px solid #ffffff; + + pre { + background-color: #ffffff; + border: none; + padding: 0px; + margin: 0px; } + } + + .nteract-md-cell { + background-color: #f5f5f5; + } + + .nteract-cell:hover.nteract-md-cell { + background-color: #ffffff; + } } diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx index 7fcdd2579..f912acd4a 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx @@ -1,6 +1,6 @@ import { actions, ContentRef } from "@nteract/core"; import { Cells, CodeCell, RawCell } from "@nteract/stateful-components"; -import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; +import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt"; import * as React from "react"; @@ -67,8 +67,8 @@ class NotebookReadOnlyRenderer extends React.Component { ? () => : undefined, editor: { - monaco: (props: PassedEditorProps) => - this.props.hideInputs ? <> : , + codemirror: (props: PassedEditorProps) => + this.props.hideInputs ? <> : , }, }} @@ -84,8 +84,8 @@ class NotebookReadOnlyRenderer extends React.Component { {{ editor: { - monaco: (props: PassedEditorProps) => - this.props.hideInputs ? <> : , + codemirror: (props: PassedEditorProps) => + this.props.hideInputs ? <> : , }, }} diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less index c680f8f56..ed77b9fb7 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.less @@ -3,110 +3,122 @@ @HighlightColor: #0078d4; .NotebookRendererContainer { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; } .NotebookRenderer { - overflow: auto; - flex-grow: 1; + overflow: auto; + flex-grow: 1; - .nteract-cells { - padding-top: 0px; + .nteract-cells { + padding-top: 0px; + } + + .nteract-cell-container { + margin-bottom: 10px; + + .nteract-cell { + padding: 0.5px; + border: 1px solid #ffffff; + border-left: 3px solid #ffffff; + + .CellContextMenuButton { + position: sticky; + z-index: 1; + top: 0px; + right: 0px; + margin: 0px 0px 0px -100%; + float: right; + visibility: hidden; + } } - .nteract-cell-container { - margin-bottom: 10px; - - .nteract-cell { - padding: 0.5px; - border: 1px solid #ffffff; - border-left: 3px solid #ffffff; - - .CellContextMenuButton { - position: sticky; - z-index: 1; - top: 0px; - right: 0px; - margin: 0px 0px 0px -100%; - float: right; - visibility: hidden; - } - } - - .CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters { - background-color: #f5f5f5; - } - - .nteract-cell:hover { - border: 1px solid @HoverColor; - border-left: 3px solid @HoverColor; - - .CellContextMenuButton { - visibility: visible; - } - } + .CodeMirror-scroll { + overflow: hidden !important; } - .nteract-cell-container.selected { - .nteract-cell { - border: 1px solid @HighlightColor; - border-left: 3px solid @HighlightColor; - } + .CodeMirror-scroll, + .CodeMirror-linenumber, + .CodeMirror-gutters { + background-color: #f5f5f5; } - // White background when hovered or selected - .nteract-cell:hover, .nteract-cell-container.selected .nteract-cell { - .CodeMirror-scroll, .CodeMirror-linenumber, .CodeMirror-gutters { - background-color: #ffffff; - } + .CodeMirror { + height: inherit; + } - .CodeMirror-linenumber { - color: #015CDA; - } + .nteract-cell:hover { + border: 1px solid @HoverColor; + border-left: 3px solid @HoverColor; - .nteract-cell-outputs { - border-top: 1px solid @HoverColor; - } + .CellContextMenuButton { + visibility: visible; + } + } + } - .nteract-md-cell { - background-color: #ffffff; - } + .nteract-cell-container.selected { + .nteract-cell { + border: 1px solid @HighlightColor; + border-left: 3px solid @HighlightColor; + } + } + + // White background when hovered or selected + .nteract-cell:hover, + .nteract-cell-container.selected .nteract-cell { + .CodeMirror-scroll, + .CodeMirror-linenumber, + .CodeMirror-gutters { + background-color: #ffffff; + } + + .CodeMirror-linenumber { + color: #015cda; } .nteract-cell-outputs { - padding: 10px; - border-top: 1px solid #ffffff; - - pre { - background-color: #ffffff; - border: none; - padding: 0px; - margin: 0px; - } + border-top: 1px solid @HoverColor; } .nteract-md-cell { - background-color: #f5f5f5; + background-color: #ffffff; } + } - .nteract-cell:hover.nteract-md-cell { - background-color: #ffffff; - } + .nteract-cell-outputs { + padding: 10px; + border-top: 1px solid #ffffff; - .nteract-md-cell .ntreact-cell-source { - width: 100%; + pre { + background-color: #ffffff; + border: none; + padding: 0px; + margin: 0px; } + } + + .nteract-md-cell { + background-color: #f5f5f5; + } + + .nteract-cell:hover.nteract-md-cell { + background-color: #ffffff; + } + + .nteract-md-cell .ntreact-cell-source { + width: 100%; + } } - // Undo tree.less .expanded::before { - content: ''; + content: ""; } .monaco-editor .monaco-list .main { - background-color: transparent; -} \ No newline at end of file + background-color: transparent; +} diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx index db3f8d5ad..792cd5e0a 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx @@ -2,7 +2,7 @@ import { CellId } from "@nteract/commutable"; import { CellType } from "@nteract/commutable/src"; import { actions, ContentRef, selectors } from "@nteract/core"; import { Cells, CodeCell, RawCell } from "@nteract/stateful-components"; -import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; +import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import * as React from "react"; import { DndProvider } from "react-dnd"; @@ -120,7 +120,9 @@ class BaseNotebookRenderer extends React.Component { {{ editor: { - monaco: (props: PassedEditorProps) => , + codemirror: (props: PassedEditorProps) => ( + + ), }, prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => ( @@ -142,7 +144,9 @@ class BaseNotebookRenderer extends React.Component { {{ editor: { - monaco: (props: PassedEditorProps) => , + codemirror: (props: PassedEditorProps) => ( + + ), }, toolbar: () => , }} @@ -157,7 +161,9 @@ class BaseNotebookRenderer extends React.Component { {{ editor: { - monaco: (props: PassedEditorProps) => , + codemirror: (props: PassedEditorProps) => ( + + ), }, toolbar: () => , }} diff --git a/src/Explorer/Notebook/notebookClientV2.test.ts b/src/Explorer/Notebook/notebookClientV2.test.ts index bb0b6bef7..4313932b5 100644 --- a/src/Explorer/Notebook/notebookClientV2.test.ts +++ b/src/Explorer/Notebook/notebookClientV2.test.ts @@ -1,8 +1,8 @@ jest.mock("./NotebookComponent/store"); jest.mock("@nteract/core"); +import { defineConfigOption } from "@nteract/mythic-configuration"; import { NotebookClientV2 } from "./NotebookClientV2"; import configureStore from "./NotebookComponent/store"; -import { defineConfigOption } from "@nteract/mythic-configuration"; describe("auto start kernel", () => { it("configure autoStartKernelOnNotebookOpen properly depending whether notebook is/is not read-only", async () => { @@ -24,6 +24,12 @@ describe("auto start kernel", () => { defaultValue: 1234, }); + defineConfigOption({ + label: "Line numbers", + key: "codeMirror.lineNumbers", + defaultValue: true, + }); + [true, false].forEach((isReadOnly) => { new NotebookClientV2({ connectionInfo: { diff --git a/src/Explorer/OpenActions.test.ts b/src/Explorer/OpenActions/OpenActions.test.tsx similarity index 97% rename from src/Explorer/OpenActions.test.ts rename to src/Explorer/OpenActions/OpenActions.test.tsx index d06e8b239..10c4580a8 100644 --- a/src/Explorer/OpenActions.test.ts +++ b/src/Explorer/OpenActions/OpenActions.test.tsx @@ -1,7 +1,7 @@ import * as ko from "knockout"; -import { ActionContracts } from "../Contracts/ExplorerContracts"; -import * as ViewModels from "../Contracts/ViewModels"; -import Explorer from "./Explorer"; +import { ActionContracts } from "../../Contracts/ExplorerContracts"; +import * as ViewModels from "../../Contracts/ViewModels"; +import Explorer from "../Explorer"; import { handleOpenAction } from "./OpenActions"; describe("OpenActions", () => { @@ -9,7 +9,6 @@ describe("OpenActions", () => { let explorer: Explorer; let database: ViewModels.Database; let collection: ViewModels.Collection; - let databases: ViewModels.Database[]; beforeEach(() => { explorer = {} as Explorer; @@ -19,7 +18,6 @@ describe("OpenActions", () => { id: ko.observable("db"), collections: ko.observableArray([]), } as ViewModels.Database; - databases = [database]; collection = { id: ko.observable("coll"), } as ViewModels.Collection; @@ -68,7 +66,7 @@ describe("OpenActions", () => { paneKind: "AddCollection", }; - const actionHandled = handleOpenAction(action, [], explorer); + handleOpenAction(action, [], explorer); expect(explorer.onNewCollectionClicked).toHaveBeenCalled(); }); @@ -78,7 +76,7 @@ describe("OpenActions", () => { paneKind: ActionContracts.PaneKind.AddCollection, }; - const actionHandled = handleOpenAction(action, [], explorer); + handleOpenAction(action, [], explorer); expect(explorer.onNewCollectionClicked).toHaveBeenCalled(); }); }); diff --git a/src/Explorer/OpenActions.ts b/src/Explorer/OpenActions/OpenActions.tsx similarity index 65% rename from src/Explorer/OpenActions.ts rename to src/Explorer/OpenActions/OpenActions.tsx index 033cb8135..ff17adf21 100644 --- a/src/Explorer/OpenActions.ts +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -1,39 +1,38 @@ // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. +import React from "react"; +import { ActionContracts } from "../../Contracts/ExplorerContracts"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { useSidePanel } from "../../hooks/useSidePanel"; +import Explorer from "../Explorer"; +import { CassandraAddCollectionPane } from "../Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; +import { SettingsPane } from "../Panes/SettingsPane/SettingsPane"; +import { CassandraAPIDataClient } from "../Tables/TableDataClient"; -import { ActionContracts } from "../Contracts/ExplorerContracts"; -import * as ViewModels from "../Contracts/ViewModels"; -import Explorer from "./Explorer"; - -export function handleOpenAction( - action: ActionContracts.DataExplorerAction, - databases: ViewModels.Database[], - explorer: Explorer -): boolean { - if ( - action.actionType === ActionContracts.ActionType.OpenCollectionTab || - (action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenCollectionTab] - ) { - openCollectionTab(action, databases); - return true; +function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string { + if (!action.query) { + return "SELECT * FROM c"; + } else if (action.query.text) { + return action.query.text; + } else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) { + let query = "SELECT * FROM c WHERE"; + for (let i = 0; i < action.query.partitionKeys.length; i++) { + const partitionKey = action.query.partitionKeys[i]; + if (!partitionKey) { + // null partition key case + query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`); + } else if (typeof partitionKey !== "string") { + // Undefined partition key case + query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`); + } else { + query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`); + } + if (i !== action.query.partitionKeys.length - 1) { + query = query.concat(" OR"); + } + } + return query; } - - if ( - action.actionType === ActionContracts.ActionType.OpenPane || - (action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenPane] - ) { - openPane(action, explorer); - return true; - } - - if ( - action.actionType === ActionContracts.ActionType.OpenSampleNotebook || - (action).actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook] - ) { - openFile(action, explorer); - return true; - } - - return false; + return "SELECT * FROM c"; } function openCollectionTab( @@ -65,7 +64,7 @@ function openCollectionTab( if ( action.tabKind === ActionContracts.TabKind.SQLDocuments || - (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments] + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments] ) { collection.onDocumentDBDocumentsClick(); break; @@ -73,7 +72,7 @@ function openCollectionTab( if ( action.tabKind === ActionContracts.TabKind.MongoDocuments || - (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments] + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments] ) { collection.onMongoDBDocumentsClick(); break; @@ -81,7 +80,7 @@ function openCollectionTab( if ( action.tabKind === ActionContracts.TabKind.SchemaAnalyzer || - (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer] + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer] ) { collection.onSchemaAnalyzerClick(); break; @@ -89,7 +88,7 @@ function openCollectionTab( if ( action.tabKind === ActionContracts.TabKind.TableEntities || - (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] ) { collection.onTableEntitiesClick(); break; @@ -97,7 +96,7 @@ function openCollectionTab( if ( action.tabKind === ActionContracts.TabKind.Graph || - (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph] + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph] ) { collection.onGraphDocumentsClick(); break; @@ -105,19 +104,19 @@ function openCollectionTab( if ( action.tabKind === ActionContracts.TabKind.SQLQuery || - (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery] + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery] ) { collection.onNewQueryClick( collection, - null, - generateQueryText(action, collection.partitionKeyProperty) + undefined, + generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperty) ); break; } if ( action.tabKind === ActionContracts.TabKind.ScaleSettings || - (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings] + action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings] ) { collection.onSettingsClick(); break; @@ -138,49 +137,59 @@ function openCollectionTab( function openPane(action: ActionContracts.OpenPane, explorer: Explorer) { if ( action.paneKind === ActionContracts.PaneKind.AddCollection || - (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection] + action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection] ) { explorer.onNewCollectionClicked(); } else if ( action.paneKind === ActionContracts.PaneKind.CassandraAddCollection || - (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection] + action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection] ) { - explorer.openCassandraAddCollectionPane(); + useSidePanel + .getState() + .openSidePanel( + "Add Table", + + ); } else if ( action.paneKind === ActionContracts.PaneKind.GlobalSettings || - (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] + action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] ) { - explorer.openSettingPane(); + useSidePanel.getState().openSidePanel("Settings", ); } } +export function handleOpenAction( + action: ActionContracts.DataExplorerAction, + databases: ViewModels.Database[], + explorer: Explorer +): boolean { + if ( + action.actionType === ActionContracts.ActionType.OpenCollectionTab || + action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenCollectionTab] + ) { + openCollectionTab(action as ActionContracts.OpenCollectionTab, databases); + return true; + } + + if ( + action.actionType === ActionContracts.ActionType.OpenPane || + action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenPane] + ) { + openPane(action as ActionContracts.OpenPane, explorer); + return true; + } + + if ( + action.actionType === ActionContracts.ActionType.OpenSampleNotebook || + action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook] + ) { + openFile(action as ActionContracts.OpenSampleNotebook, explorer); + return true; + } + + return false; +} + function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) { explorer.handleOpenFileAction(decodeURIComponent(action.path)); } - -function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string { - if (!action.query) { - return "SELECT * FROM c"; - } else if (!!action.query.text) { - return action.query.text; - } else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) { - let query = "SELECT * FROM c WHERE"; - for (let i = 0; i < action.query.partitionKeys.length; i++) { - let partitionKey = action.query.partitionKeys[i]; - if (!partitionKey) { - // null partition key case - query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`); - } else if (typeof partitionKey !== "string") { - // Undefined partition key case - query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`); - } else { - query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`); - } - if (i !== action.query.partitionKeys.length - 1) { - query = query.concat(" OR"); - } - } - return query; - } - return "SELECT * FROM c"; -} diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index aa98e4113..a946c1091 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -31,6 +31,7 @@ import { getUpsellMessage } from "../../Utils/PricingUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { PanelFooterComponent } from "./PanelFooterComponent"; import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent"; import { PanelLoadingScreen } from "./PanelLoadingScreen"; @@ -125,6 +126,8 @@ export class AddCollectionPanel extends React.Component {this.state.errorMessage && ( @@ -137,7 +140,7 @@ export class AddCollectionPanel extends React.Component (this.newDatabaseThroughput = throughput)} @@ -469,9 +470,7 @@ export class AddCollectionPanel extends React.Component (this.collectionThroughput = throughput)} @@ -680,7 +679,7 @@ export class AddCollectionPanel extends React.Component ({ + return useDatabases.getState().databases?.map((database) => ({ key: database.id(), text: database.id(), })); @@ -772,9 +771,9 @@ export class AddCollectionPanel extends React.Component database.id() === this.state.selectedDatabaseId); + const selectedDatabase = useDatabases + .getState() + .databases?.find((database) => database.id() === this.state.selectedDatabaseId); return !!selectedDatabase?.offer(); } diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx index c57d8ec89..3ee59bd30 100644 --- a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx @@ -16,7 +16,9 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { getUpsellMessage } from "../../../Utils/PricingUtils"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; +import { getTextFieldStyles } from "../PanelStyles"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; export interface AddDatabasePaneProps { @@ -171,7 +173,12 @@ export const AddDatabasePanel: FunctionComponent = ({ {!formErrors && isFreeTierAccount && ( = ({ /> )}
-
+ - * - {databaseIdLabel} + + + {databaseIdLabel} + {databaseIdTooltipText} @@ -199,36 +208,37 @@ export const AddDatabasePanel: FunctionComponent = ({ value={databaseId} onChange={handleonChangeDBId} autoFocus - style={{ fontSize: 12 }} - styles={{ root: { width: 300 } }} + styles={getTextFieldStyles()} /> - - setDatabaseCreateNewShared(!databaseCreateNewShared)} - /> - {databaseLevelThroughputTooltipText} - - - {!isServerlessAccount() && databaseCreateNewShared && ( - (throughput = newThroughput)} - setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)} - onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)} - /> + {!isServerlessAccount() && ( + + setDatabaseCreateNewShared(!databaseCreateNewShared)} + /> + {databaseLevelThroughputTooltipText} + )} -
+ + + {!isServerlessAccount() && databaseCreateNewShared && ( + (throughput = newThroughput)} + setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)} + onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)} + /> + )}
); diff --git a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap index 75e11242d..9c4bbaa32 100644 --- a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap +++ b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap @@ -10,16 +10,17 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
-
+ - * + *  Database id @@ -38,13 +39,16 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = ` pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]" placeholder="Type a new database id" size={40} - style={ - Object { - "fontSize": 12, - } - } styles={ Object { + "field": Object { + "fontSize": 12, + "selectors": Object { + "::placeholder": Object { + "fontSize": 12, + }, + }, + }, "root": Object { "width": 300, }, @@ -82,14 +86,14 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = ` Provisioned throughput at the database level will be shared across all collections within the database. - -
+ +
`; diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx index e052a6616..834bbed18 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx @@ -1,14 +1,16 @@ import { mount } from "enzyme"; import * as ko from "knockout"; import React from "react"; +import { SavedQueries } from "../../../Common/Constants"; import { QueriesClient } from "../../../Common/QueriesClient"; import { Query } from "../../../Contracts/DataModels"; +import { Collection, Database } from "../../../Contracts/ViewModels"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { BrowseQueriesPane } from "./BrowseQueriesPane"; describe("Browse queries panel", () => { const fakeExplorer = {} as Explorer; - fakeExplorer.canSaveQueries = ko.computed(() => true); const fakeClientQuery = {} as QueriesClient; const fakeQueryData = [] as Query[]; fakeClientQuery.getQueries = async () => fakeQueryData; @@ -17,6 +19,16 @@ describe("Browse queries panel", () => { explorer: fakeExplorer, closePanel: (): void => undefined, }; + useDatabases.getState().addDatabases([ + { + id: ko.observable(SavedQueries.DatabaseName), + collections: ko.observableArray([ + { + id: ko.observable(SavedQueries.CollectionName), + } as Collection, + ]), + } as Database, + ]); it("Should render Default properly", () => { const wrapper = mount(); diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx index dead62b56..ce23077be 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx @@ -12,7 +12,9 @@ import { QueriesGridComponentProps, } from "../../Controls/QueriesGridReactComponent/QueriesGridComponent"; import Explorer from "../../Explorer"; -import QueryTab from "../../Tabs/QueryTab"; +import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; +import { useDatabases } from "../../useDatabases"; +import { useSelectedNode } from "../../useSelectedNode"; interface BrowseQueriesPaneProps { explorer: Explorer; @@ -23,7 +25,7 @@ export const BrowseQueriesPane: FunctionComponent = ({ }: BrowseQueriesPaneProps): JSX.Element => { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const loadSavedQuery = (savedQuery: Query): void => { - const selectedCollection: Collection = explorer && explorer.findSelectedCollection(); + const selectedCollection: Collection = useSelectedNode.getState().findSelectedCollection(); if (!selectedCollection) { // should never get into this state because this pane is only accessible through the query tab logError("No collection was selected", "BrowseQueriesPane.loadSavedQuery"); @@ -31,13 +33,13 @@ export const BrowseQueriesPane: FunctionComponent = ({ } else if (userContext.apiType === "Mongo") { selectedCollection.onNewMongoQueryClick(selectedCollection, undefined); } else { - selectedCollection.onNewQueryClick(selectedCollection, undefined); + selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query); } - const queryTab = explorer.tabsManager.activeTab() as QueryTab; + + const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab); queryTab.tabTitle(savedQuery.queryName); queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`); - queryTab.initialEditorContent(savedQuery.query); - queryTab.sqlQueryEditorContent(savedQuery.query); + trace(Action.LoadSavedQuery, ActionModifiers.Mark, { dataExplorerArea: Areas.ContextualPane, queryName: savedQuery.queryName, @@ -45,12 +47,13 @@ export const BrowseQueriesPane: FunctionComponent = ({ }); closeSidePanel(); }; + const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled); const props: QueriesGridComponentProps = { queriesClient: explorer.queriesClient, onQuerySelect: loadSavedQuery, containerVisible: true, - saveQueryEnabled: explorer.canSaveQueries(), + saveQueryEnabled: isSaveQueryEnabled(), }; return ( diff --git a/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap b/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap index eed894482..5fcb72a63 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap +++ b/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Browse queries panel Should render Default properly 1`] = ` closePanel={[Function]} explorer={ Object { - "canSaveQueries": [Function], "queriesClient": Object { "getQueries": [Function], }, diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.test.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.test.tsx index 20433bac5..f9624ac80 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.test.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.test.tsx @@ -1,32 +1,30 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import { shallow } from "enzyme"; import React from "react"; import Explorer from "../../Explorer"; import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; import { CassandraAddCollectionPane } from "./CassandraAddCollectionPane"; -const props = { - explorer: new Explorer(), - closePanel: (): void => undefined, - cassandraApiClient: new CassandraAPIDataClient(), -}; -describe("CassandraAddCollectionPane Pane", () => { +describe("Cassandra add collection pane test", () => { + const props = { + explorer: new Explorer(), + closePanel: (): void => undefined, + cassandraApiClient: new CassandraAPIDataClient(), + }; + beforeEach(() => render()); - it("should render Default properly", () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - it("click on is Create new keyspace", () => { - fireEvent.click(screen.getByLabelText("Create new keyspace")); - expect(screen.getByLabelText("Provision keyspace throughput")).toBeDefined(); - }); - it("click on Use existing", () => { - fireEvent.click(screen.getByLabelText("Use existing keyspace")); + it("should render default properly", () => { + expect(screen.getByRole("radio", { name: "Create new keyspace", checked: true })).toBeDefined(); + expect(screen.getByRole("checkbox", { name: "Provision shared throughput", checked: false })).toBeDefined(); }); - it("Enter Keyspace name ", () => { - fireEvent.change(screen.getByLabelText("Keyspace id"), { target: { value: "unittest1" } }); - expect(screen.getByLabelText("CREATE TABLE unittest1.")).toBeDefined(); + it("click on use existing", () => { + fireEvent.click(screen.getByRole("radio", { name: "Use existing keyspace" })); + expect(screen.getByRole("combobox", { name: "Choose existing keyspace id" })).toBeDefined(); + }); + + it("enter Keyspace name ", () => { + fireEvent.change(screen.getByRole("textbox", { name: "Keyspace id" }), { target: { value: "table1" } }); + expect(screen.getByText("CREATE TABLE table1.")).toBeDefined(); }); }); diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index bef852942..7d0703336 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -1,21 +1,19 @@ -import { Label, Stack, TextField } from "@fluentui/react"; -import React, { FunctionComponent, useEffect, useState } from "react"; -import * as _ from "underscore"; +import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react"; +import React, { FunctionComponent, useState } from "react"; import * as Constants from "../../../Common/Constants"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; -import * as DataModels from "../../../Contracts/DataModels"; -import * as ViewModels from "../../../Contracts/ViewModels"; import { useSidePanel } from "../../../hooks/useSidePanel"; -import * as AddCollectionUtility from "../../../Shared/AddCollectionUtility"; import * as SharedConstants from "../../../Shared/Constants"; -import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../../UserContext"; -import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; +import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../../Explorer"; import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; +import { useDatabases } from "../../useDatabases"; +import { getTextFieldStyles } from "../PanelStyles"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; export interface CassandraAddCollectionPaneProps { @@ -27,183 +25,73 @@ export const CassandraAddCollectionPane: FunctionComponent { + let newKeySpaceThroughput: number; + let isNewKeySpaceAutoscale: boolean; + let tableThroughput: number; + let isTableAutoscale: boolean; + let isCostAcknowledged: boolean; + const closeSidePanel = useSidePanel((state) => state.closeSidePanel); - const throughputDefaults = userContext.collectionCreationDefaults.throughput; - const [createTableQuery, setCreateTableQuery] = useState("CREATE TABLE "); - const [keyspaceId, setKeyspaceId] = useState(""); + const [newKeyspaceId, setNewKeyspaceId] = useState(""); + const [existingKeyspaceId, setExistingKeyspaceId] = useState(""); const [tableId, setTableId] = useState(""); - const [throughput, setThroughput] = useState( - AddCollectionUtility.getMaxThroughput(userContext.collectionCreationDefaults, container) - ); - - const [isAutoPilotSelected, setIsAutoPilotSelected] = useState(userContext.features.autoscaleDefault); - - const [isSharedAutoPilotSelected, setIsSharedAutoPilotSelected] = useState( - userContext.features.autoscaleDefault - ); - const [userTableQuery, setUserTableQuery] = useState( "(userid int, name text, email text, PRIMARY KEY (userid))" ); - - const [keyspaceHasSharedOffer, setKeyspaceHasSharedOffer] = useState(false); - const [keyspaceIds, setKeyspaceIds] = useState([]); - const [keyspaceThroughput, setKeyspaceThroughput] = useState(throughputDefaults.shared); + const [isKeyspaceShared, setIsKeyspaceShared] = useState(false); const [keyspaceCreateNew, setKeyspaceCreateNew] = useState(true); const [dedicateTableThroughput, setDedicateTableThroughput] = useState(false); - const [throughputSpendAck, setThroughputSpendAck] = useState(false); - const [sharedThroughputSpendAck, setSharedThroughputSpendAck] = useState(false); - - const { minAutoPilotThroughput: selectedAutoPilotThroughput } = AutoPilotUtils; - const { minAutoPilotThroughput: sharedAutoPilotThroughput } = AutoPilotUtils; - - const _getAutoPilot = (): DataModels.AutoPilotCreationSettings => { - if (keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected && sharedAutoPilotThroughput) { - return { - maxThroughput: sharedAutoPilotThroughput * 1, - }; - } - - if (selectedAutoPilotThroughput) { - return { - maxThroughput: selectedAutoPilotThroughput * 1, - }; - } - - return undefined; - }; - - const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier; - - const canConfigureThroughput = !container.isServerlessEnabled(); - - const keyspaceOffers = new Map(); const [isExecuting, setIsExecuting] = useState(); - const [formErrors, setFormErrors] = useState(""); - - useEffect(() => { - if (keyspaceIds.indexOf(keyspaceId) >= 0) { - setKeyspaceHasSharedOffer(keyspaceOffers.has(keyspaceId)); - } - setCreateTableQuery(`CREATE TABLE ${keyspaceId}.`); - }, [keyspaceId]); + const [formError, setFormError] = useState(""); + const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier; const addCollectionPaneOpenMessage = { collection: { id: tableId, storage: Constants.BackendDefaults.multiPartitionStorageInGb, - offerThroughput: throughput, + offerThroughput: newKeySpaceThroughput || tableThroughput, partitionKey: "", - databaseId: keyspaceId, + databaseId: keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId, }, subscriptionType: userContext.subscriptionType, subscriptionQuotaId: userContext.quotaId, defaultsCheck: { storage: "u", - throughput, + throughput: newKeySpaceThroughput || tableThroughput, flight: userContext.addCollectionFlight, }, dataExplorerArea: Constants.Areas.ContextualPane, }; - useEffect(() => { - if (!container.isServerlessEnabled()) { - setIsAutoPilotSelected(userContext.features.autoscaleDefault); - } - - TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage); - }, []); - - useEffect(() => { - if (container) { - const newKeyspaceIds: ViewModels.Database[] = container.databases(); - const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => { - if (keyspace && keyspace.offer && !!keyspace.offer()) { - keyspaceOffers.set(keyspace.id(), keyspace.offer()); - } - return keyspace.id(); - }); - setKeyspaceIds(cachedKeyspaceIdsList); - } - }, []); - - const _isValid = () => { - const sharedAutoscaleThroughput = sharedAutoPilotThroughput * 1; - if ( - isSharedAutoPilotSelected && - sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && - !sharedThroughputSpendAck - ) { - setFormErrors(`Please acknowledge the estimated monthly spend.`); - return false; - } - - const dedicatedAutoscaleThroughput = selectedAutoPilotThroughput * 1; - if ( - isAutoPilotSelected && - dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && - !throughputSpendAck - ) { - setFormErrors(`Please acknowledge the estimated monthly spend.`); - return false; - } - - if ((keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected) || isAutoPilotSelected) { - const autoPilot = _getAutoPilot(); - if ( - !autoPilot || - !autoPilot.maxThroughput || - !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput) - ) { - setFormErrors( - `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` - ); - return false; - } - return true; - } - - if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) { - setFormErrors(`Please acknowledge the estimated daily spend.`); - return false; - } - - if ( - keyspaceHasSharedOffer && - keyspaceCreateNew && - keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && - !sharedThroughputSpendAck - ) { - setFormErrors("Please acknowledge the estimated daily spend"); - return false; - } - - return true; - }; - const onSubmit = async () => { - if (!_isValid()) { + const throughput = keyspaceCreateNew ? newKeySpaceThroughput : tableThroughput; + const keyspaceId = keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId; + + if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { + const errorMessage = + isNewKeySpaceAutoscale || isTableAutoscale + ? "Please acknowledge the estimated monthly spend." + : "Please acknowledge the estimated daily spend."; + setFormError(errorMessage); return; } + setIsExecuting(true); const autoPilotCommand = `cosmosdb_autoscale_max_throughput`; - - const toCreateKeyspace: boolean = keyspaceCreateNew; - const useAutoPilotForKeyspace: boolean = isSharedAutoPilotSelected && !!sharedAutoPilotThroughput; const createKeyspaceQueryPrefix = `CREATE KEYSPACE ${keyspaceId.trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`; - const createKeyspaceQuery: string = keyspaceHasSharedOffer - ? useAutoPilotForKeyspace - ? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${keyspaceThroughput};` - : `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${keyspaceThroughput};` + const createKeyspaceQuery: string = isKeyspaceShared + ? isNewKeySpaceAutoscale + ? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${newKeySpaceThroughput};` + : `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${newKeySpaceThroughput};` : `${createKeyspaceQueryPrefix};`; let tableQuery: string; - const createTableQueryPrefix = `${createTableQuery}${tableId.trim()} ${userTableQuery}`; + const createTableQueryPrefix = `CREATE TABLE ${keyspaceId}.${tableId.trim()} ${userTableQuery}`; - if (canConfigureThroughput && (dedicateTableThroughput || !keyspaceHasSharedOffer)) { - if (isAutoPilotSelected && selectedAutoPilotThroughput) { - tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${throughput};`; + if (tableThroughput) { + if (isTableAutoscale) { + tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${tableThroughput};`; } else { - tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${throughput};`; + tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${tableThroughput};`; } } else { tableQuery = `${createTableQueryPrefix};`; @@ -215,15 +103,15 @@ export const CassandraAddCollectionPane: FunctionComponent, mode: string): void => { - setKeyspaceCreateNew(mode === "Create new"); - }; const props: RightPaneFormProps = { - formError: formErrors, + formError, isExecuting, - submitButtonText: "Apply", + submitButtonText: "OK", onSubmit, }; + return ( -
-
-

-

+ + + + Keyspace name Select an existing keyspace or enter a new keyspace id. - -

+
+
handleOnChangeKeyspaceType(e, "Create new")} + onChange={() => { + setKeyspaceCreateNew(true); + setIsKeyspaceShared(false); + setExistingKeyspaceId(""); + }} /> - Create new + Create new handleOnChangeKeyspaceType(e, "Use existing")} + onChange={() => { + setKeyspaceCreateNew(false); + setIsKeyspaceShared(false); + }} + /> + Use existing + + + {keyspaceCreateNew && ( + + setNewKeyspaceId(newValue)} + ariaLabel="Keyspace id" + autoFocus + /> + + {!isServerlessAccount() && ( + + , isChecked: boolean) => setIsKeyspaceShared(isChecked)} + /> + + Provisioned throughput at the keyspace level will be shared across unlimited number of tables within + the keyspace + + + )} + + )} + + {!keyspaceCreateNew && ( + ({ + key: keyspace.id(), + text: keyspace.id(), + data: { + isShared: !!keyspace.offer(), + }, + }))} + onChange={(event: React.FormEvent, option: IDropdownOption) => { + setExistingKeyspaceId(option.key as string); + setIsKeyspaceShared(option.data.isShared); + }} + responsiveMode={999} + /> + )} + + {!isServerlessAccount() && keyspaceCreateNew && isKeyspaceShared && ( + (newKeySpaceThroughput = throughput)} + setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)} + onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)} + /> + )} +
+ + + + + + Enter CQL command to create the table.{" "} + + Learn More + + + + + + + {`CREATE TABLE ${keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId}.`} + + setTableId(newValue)} /> - Use existing setKeyspaceId(newValue)} - ariaLabel="Keyspace id" - autoFocus - /> - - {keyspaceIds?.map((id: string, index: number) => ( - - ))} - - {canConfigureThroughput && keyspaceCreateNew && ( -
- setKeyspaceHasSharedOffer(e.target.checked)} - /> - - Provision keyspace throughput - - - Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the - keyspace - -
- )} - {canConfigureThroughput && keyspaceCreateNew && keyspaceHasSharedOffer && ( -
- setKeyspaceThroughput(throughput)} - setIsAutoscale={(isAutoscale: boolean) => setIsSharedAutoPilotSelected(isAutoscale)} - onCostAcknowledgeChange={(isAcknowledge: boolean) => { - setSharedThroughputSpendAck(isAcknowledge); - }} - /> -
- )} -
-
-

- -

-
- {createTableQuery} -
- setTableId(newValue)} - style={{ marginBottom: "5px" }} - /> - setUserTableQuery(newValue)} /> -
+ - {canConfigureThroughput && keyspaceHasSharedOffer && !keyspaceCreateNew && ( -
+ {!isServerlessAccount() && isKeyspaceShared && !keyspaceCreateNew && ( + -
+ )} - {canConfigureThroughput && (!keyspaceHasSharedOffer || dedicateTableThroughput) && ( -
- setThroughput(throughput)} - setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)} - onCostAcknowledgeChange={(isAcknowledge: boolean) => { - setThroughputSpendAck(isAcknowledge); - }} - /> -
+ {!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && ( + (tableThroughput = throughput)} + setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)} + onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)} + /> )}
diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/__snapshots__/CassandraAddCollectionPane.test.tsx.snap b/src/Explorer/Panes/CassandraAddCollectionPane/__snapshots__/CassandraAddCollectionPane.test.tsx.snap deleted file mode 100644 index d3f41614e..000000000 --- a/src/Explorer/Panes/CassandraAddCollectionPane/__snapshots__/CassandraAddCollectionPane.test.tsx.snap +++ /dev/null @@ -1,163 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CassandraAddCollectionPane Pane should render Default properly 1`] = ` - -
-
-

- - Keyspace name - - Select an existing keyspace or enter a new keyspace id. - - -

- - - - Create new - - - - Use existing - - - - -
- - - Provision keyspace throughput - - - Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the keyspace - -
-
-
-

- - Enter CQL command to create the table. - - Learn More - - -

-
- CREATE TABLE -
- - -
-
- -
-
-
-`; diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx index c75f998e3..1ad1ddf12 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx @@ -1,54 +1,53 @@ jest.mock("../../../Common/dataAccess/deleteCollection"); jest.mock("../../../Shared/Telemetry/TelemetryProcessor"); -import { mount, ReactWrapper, shallow } from "enzyme"; +import { mount, shallow } from "enzyme"; import * as ko from "knockout"; import React from "react"; import { deleteCollection } from "../../../Common/dataAccess/deleteCollection"; import DeleteFeedback from "../../../Common/DeleteFeedback"; import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels"; -import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels"; +import { Collection, Database } from "../../../Contracts/ViewModels"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../../../UserContext"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; +import { useSelectedNode } from "../../useSelectedNode"; import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane"; describe("Delete Collection Confirmation Pane", () => { - describe("Explorer.isLastCollection()", () => { - let explorer: Explorer; - - beforeEach(() => { - explorer = new Explorer(); - }); + describe("useDatabases.isLastCollection()", () => { + beforeAll(() => useDatabases.getState().clearDatabases()); + afterEach(() => useDatabases.getState().clearDatabases()); it("should be true if 1 database and 1 collection", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastCollection()).toBe(true); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + useDatabases.getState().addDatabases([database]); + expect(useDatabases.getState().isLastCollection()).toBe(true); }); it("should be false if if 1 database and 2 collection", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection, {} as Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastCollection()).toBe(false); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([ + { id: ko.observable("coll1") } as Collection, + { id: ko.observable("coll2") } as Collection, + ]); + useDatabases.getState().addDatabases([database]); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); it("should be false if 2 database and 1 collection each", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - const database2 = {} as Database; - database2.collections = ko.observableArray([{} as Collection]); - explorer.databases = ko.observableArray([database, database2]); - expect(explorer.isLastCollection()).toBe(false); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("coll1") } as Collection]); + const database2 = { id: ko.observable("testDB2") } as Database; + database2.collections = ko.observableArray([{ id: ko.observable("coll2") } as Collection]); + useDatabases.getState().addDatabases([database, database2]); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); it("should be false if 0 databases", () => { - const database = {} as Database; - explorer.databases = ko.observableArray(); - database.collections = ko.observableArray(); - expect(explorer.isLastCollection()).toBe(false); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); }); @@ -56,46 +55,39 @@ describe("Delete Collection Confirmation Pane", () => { it("should return true if last collection and database does not have shared throughput else false", () => { const fakeExplorer = new Explorer(); fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; - fakeExplorer.isSelectedDatabaseShared = () => false; - const props = { - explorer: fakeExplorer, - closePanel: (): void => undefined, - collectionName: "container", - }; - const wrapper = shallow(); - expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); - - props.explorer.isLastCollection = () => true; - props.explorer.isSelectedDatabaseShared = () => true; - wrapper.setProps(props); + const wrapper = shallow(); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); - props.explorer.isLastCollection = () => false; - props.explorer.isSelectedDatabaseShared = () => false; - wrapper.setProps(props); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + database.nodeKind = "Database"; + database.isDatabaseShared = ko.computed(() => false); + useDatabases.getState().addDatabases([database]); + useSelectedNode.getState().setSelectedNode(database); + wrapper.setProps({ explorer: fakeExplorer }); + expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); + + database.isDatabaseShared = ko.computed(() => true); + wrapper.setProps({ explorer: fakeExplorer }); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); }); }); describe("submit()", () => { - let wrapper: ReactWrapper; const selectedCollectionId = "testCol"; const databaseId = "testDatabase"; const fakeExplorer = {} as Explorer; - fakeExplorer.findSelectedCollection = () => { - return { - id: ko.observable(selectedCollectionId), - databaseId, - rid: "test", - } as Collection; - }; - fakeExplorer.selectedCollectionId = ko.computed(() => selectedCollectionId); - fakeExplorer.selectedNode = ko.observable(); fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; - fakeExplorer.isSelectedDatabaseShared = () => false; + const database = { id: ko.observable(databaseId) } as Database; + const collection = { + id: ko.observable(selectedCollectionId), + nodeKind: "Collection", + database, + databaseId, + } as Collection; + database.collections = ko.observableArray([collection]); + database.isDatabaseShared = ko.computed(() => false); beforeAll(() => { updateUserContext({ @@ -113,15 +105,17 @@ describe("Delete Collection Confirmation Pane", () => { }); beforeEach(() => { - const props = { - explorer: fakeExplorer, - closePanel: (): void => undefined, - collectionName: "container", - }; - wrapper = mount(); + useDatabases.getState().addDatabases([database]); + useSelectedNode.getState().setSelectedNode(collection); + }); + + afterEach(() => { + useDatabases.getState().clearDatabases(); + useSelectedNode.getState().setSelectedNode(undefined); }); it("should call delete collection", () => { + const wrapper = mount(); expect(wrapper).toMatchSnapshot(); expect(wrapper.exists("#confirmCollectionId")).toBe(true); @@ -138,6 +132,7 @@ describe("Delete Collection Confirmation Pane", () => { }); it("should record feedback", async () => { + const wrapper = mount(); expect(wrapper.exists("#confirmCollectionId")).toBe(true); wrapper .find("#confirmCollectionId") diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index 8fad674bd..6effe0b01 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -13,7 +13,10 @@ import { userContext } from "../../../UserContext"; import { getCollectionName } from "../../../Utils/APITypeUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; +import { useSelectedNode } from "../../useSelectedNode"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; + export interface DeleteCollectionConfirmationPaneProps { explorer: Explorer; } @@ -27,13 +30,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent(""); const [isExecuting, setIsExecuting] = useState(false); - const shouldRecordFeedback = (): boolean => { - return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared(); - }; + const shouldRecordFeedback = (): boolean => + useDatabases.getState().isLastCollection() && + !useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared(); + const collectionName = getCollectionName().toLocaleLowerCase(); const paneTitle = "Delete " + collectionName; const onSubmit = async (): Promise => { - const collection = explorer.findSelectedCollection(); + const collection = useSelectedNode.getState().findSelectedCollection(); if (!collection || inputCollectionName !== collection.id()) { const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName; setFormError(errorMessage); @@ -58,7 +62,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId ); diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index 5032adad2..6b3104666 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -2,16 +2,9 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = ` @@ -43,7 +36,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect variant="small" > Confirm by typing the container @@ -347,18 +340,18 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect value="" >
-
- - - Help us improve Azure Cosmos DB! - - - - - What is the reason why you are deleting this - container - ? - - - - -
-
-
-