diff --git a/.env.example b/.env.example index 62538cbc0..ea79c9a84 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1 @@ -PORTAL_RUNNER_USERNAME= -PORTAL_RUNNER_PASSWORD= -PORTAL_RUNNER_SUBSCRIPTION= -PORTAL_RUNNER_RESOURCE_GROUP= -PORTAL_RUNNER_DATABASE_ACCOUNT= -PORTAL_RUNNER_DATABASE_ACCOUNT_KEY= -PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT= -PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY= -PORTAL_RUNNER_CONNECTION_STRING= -NOTEBOOKS_TEST_RUNNER_TENANT_ID= -NOTEBOOKS_TEST_RUNNER_CLIENT_ID= -NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET= -CASSANDRA_CONNECTION_STRING= -MONGO_CONNECTION_STRING= -TABLES_CONNECTION_STRING= DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index f4064b9e5..7a5d06bbf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -71,7 +71,6 @@ src/Explorer/DataSamples/ContainerSampleGenerator.test.ts src/Explorer/DataSamples/ContainerSampleGenerator.ts src/Explorer/DataSamples/DataSamplesUtil.test.ts src/Explorer/DataSamples/DataSamplesUtil.ts -src/Explorer/Explorer.tsx src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts @@ -83,11 +82,6 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts -# src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts -# src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts - -src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts -src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts src/Explorer/Menus/ContextMenu.ts src/Explorer/MostRecentActivity/MostRecentActivity.ts src/Explorer/Notebook/NotebookClientV2.ts @@ -141,7 +135,6 @@ src/Explorer/Tabs/TabsBase.ts src/Explorer/Tabs/TriggerTab.ts src/Explorer/Tabs/UserDefinedFunctionTab.ts src/Explorer/Tree/AccessibleVerticalList.ts -src/Explorer/Tree/Collection.test.ts src/Explorer/Tree/Collection.ts src/Explorer/Tree/ConflictId.ts src/Explorer/Tree/DocumentId.ts @@ -150,65 +143,32 @@ src/Explorer/Tree/ResourceTokenCollection.ts src/Explorer/Tree/StoredProcedure.ts src/Explorer/Tree/TreeComponents.ts src/Explorer/Tree/Trigger.ts -src/Explorer/Tree/UserDefinedFunction.ts src/Explorer/WaitsForTemplateViewModel.ts src/GitHub/GitHubClient.test.ts src/GitHub/GitHubClient.ts src/GitHub/GitHubConnector.ts -src/GitHub/GitHubContentProvider.test.ts -src/GitHub/GitHubContentProvider.ts src/GitHub/GitHubOAuthService.ts src/Index.ts src/Juno/JunoClient.test.ts src/Juno/JunoClient.ts src/Platform/Hosted/Authorization.ts -src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts src/ReactDevTools.ts src/Shared/Constants.ts src/Shared/DefaultExperienceUtility.test.ts src/Shared/DefaultExperienceUtility.ts -src/Shared/ExplorerSettings.ts -src/Shared/PriceEstimateCalculator.ts -src/Shared/StorageUtility.test.ts -src/Shared/StorageUtility.ts src/Shared/appInsights.ts src/SparkClusterManager/ArcadiaResourceManager.ts src/SparkClusterManager/SparkClusterManager.ts src/Terminal/JupyterLabAppFactory.ts src/Terminal/NotebookAppContracts.d.ts -src/Terminal/index.ts -src/TokenProviders/PortalTokenProvider.ts -src/TokenProviders/TokenProviderFactory.ts -src/Utils/PricingUtils.test.ts -src/Utils/QueryUtils.test.ts src/applyExplorerBindings.ts src/global.d.ts src/setupTests.ts -src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx -src/Explorer/Controls/Accordion/AccordionComponent.tsx -src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.test.tsx -src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx -src/Explorer/Controls/AccountSwitch/AccountSwitchComponentAdapter.tsx -src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx -src/Explorer/Controls/CollapsiblePanel/CollapsiblePanel.tsx -src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx -src/Explorer/Controls/DialogReactComponent/DialogComponent.tsx -src/Explorer/Controls/DialogReactComponent/DialogComponentAdapter.tsx -src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.test.tsx -src/Explorer/Controls/Directory/DefaultDirectoryDropdownComponent.tsx -src/Explorer/Controls/Directory/DirectoryComponentAdapter.tsx -src/Explorer/Controls/Directory/DirectoryListComponent.test.tsx -src/Explorer/Controls/Directory/DirectoryListComponent.tsx -src/Explorer/Controls/Editor/EditorReact.tsx src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx -src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx -src/NotebookViewer/NotebookViewer.tsx src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx src/Explorer/Controls/TreeComponent/TreeComponent.tsx -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/GraphVizComponent.tsx @@ -216,43 +176,19 @@ src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx -src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx -src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx -src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx -src/Explorer/Notebook/NotebookComponent/NotebookComponent.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx -src/Explorer/Notebook/NotebookComponent/contents/file/index.tsx -src/Explorer/Notebook/NotebookComponent/contents/file/text-file.tsx src/Explorer/Notebook/NotebookComponent/contents/index.tsx -src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx -src/Explorer/Notebook/NotebookRenderer/Prompt.tsx -src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx -src/Explorer/Notebook/NotebookRenderer/StatusBar.test.tsx -src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx -src/Explorer/Notebook/NotebookRenderer/Toolbar.tsx -src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx -src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx -src/Explorer/Notebook/NotebookRenderer/decorators/HoverableCell.tsx src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx -src/Explorer/Notebook/temp/inputs/editor.tsx -src/Explorer/Notebook/temp/markdown-cell.tsx -src/Explorer/Notebook/temp/source.tsx -src/Explorer/Notebook/temp/syntax-highlighter/index.tsx -src/Explorer/SplashScreen/SplashScreen.tsx -src/Explorer/Tabs/GalleryTab.tsx -src/Explorer/Tabs/NotebookViewerTab.tsx -src/Explorer/Tabs/TerminalTab.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx -src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx __mocks__/monaco-editor.ts -src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx \ No newline at end of file +src/Explorer/Tree/ResourceTree.tsx \ No newline at end of file diff --git a/configs/mpac.json b/configs/mpac.json index d8e5604d2..7c270c6d5 100644 --- a/configs/mpac.json +++ b/configs/mpac.json @@ -1,4 +1,3 @@ { - "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com", - "enableSchemaAnalyzer": true + "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com" } \ No newline at end of file diff --git a/less/resourceTree.less b/less/resourceTree.less index cac3f049f..39bced9da 100644 --- a/less/resourceTree.less +++ b/less/resourceTree.less @@ -2,6 +2,7 @@ .dataResourceTree { margin-left: @MediumSpace; + overflow: auto; .databaseHeader { font-size: 14px; diff --git a/less/tree.less b/less/tree.less index e60bcf69c..ed0fbf71f 100644 --- a/less/tree.less +++ b/less/tree.less @@ -1,273 +1,270 @@ @import "./Common/Constants"; - .resourceTree { + height: 100%; + flex: 0 0 auto; + .main { height: 100%; - width: 20%; - flex: 0 0 auto; - .main { - height: 100%; - } + } } .resourceTreeScroll { - height: 100%; - display: flex; - overflow-y: auto; - overflow-x: hidden; - padding-right: 10px; + height: 100%; + display: flex; + overflow-y: auto; + overflow-x: hidden; + padding-right: 10px; } .userSelectNone { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; } .treeHovermargin { - margin-left: 16px; + margin-left: 16px; } .highlight { - padding: @SmallSpace 2px; - outline: 0; + padding: @SmallSpace 2px; + outline: 0; - &:hover { - .hover(); - } + &:hover { + .hover(); + } - &:active { - .active(); - } + &:active { + .active(); + } - &:focus { - .focus(); - } + &:focus { + .focus(); + } } .contextmenushowing { - background-color: #EEE; + background-color: #eee; } .collectionstree { - width: 100%; - margin-top: @DefaultSpace; + width: 100%; + margin-top: @DefaultSpace; + .databaseList { + list-style-type: none; + padding-left: 0px; - .databaseList { - list-style-type: none; - padding-left: 0px; - - .collectionList { - padding-left:(2 * @MediumSpace); - } - - .collectionChildList { - padding-left: @LargeSpace; - } - - .databaseDocuments { - padding-left: (5 * @MediumSpace); - } + .collectionList { + padding-left: (2 * @MediumSpace); } + + .collectionChildList { + padding-left: @LargeSpace; + } + + .databaseDocuments { + padding-left: (5 * @MediumSpace); + } + } } .pointerCursor { - cursor: pointer; + cursor: pointer; } .menuEllipsis { - padding-right: 6px; - font-weight: bold; - font-size: 18px; - position: relative; - top: -5px; - left: 0px; - float: right; - display: none; - padding-left: 6px!important; - line-height: @TreeLineHeight; + padding-right: 6px; + font-weight: bold; + font-size: 18px; + position: relative; + top: -5px; + left: 0px; + float: right; + display: none; + padding-left: 6px !important; + line-height: @TreeLineHeight; } .databaseMenu { - .flex-display(); + .flex-display(); } .databaseMenu:hover .menuEllipsis, .databaseMenu:focus .menuEllipsis { - display: block; + display: block; } .databaseCollChildTextOverflow { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - flex: 1; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + flex: 1; } .collectionMenu { - .flex-display(); + .flex-display(); } .collectionMenu:hover .menuEllipsis, .collectionMenu:focus .menuEllipsis { - display: block; + display: block; } .documentsMenu:hover .menuEllipsis, .documentsMenu:focus .menuEllipsis { - display: block; + display: block; } .treeChildMenu { - display: flex; + display: flex; } .storedProcedureMenu:hover .menuEllipsis, .storedProcedureMenu:focus .menuEllipsis { - display: block; + display: block; } .childMenu { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-left: (6 * @MediumSpace); - width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: (6 * @MediumSpace); + width: 100%; } .storedChildMenu:hover .menuEllipsis, .storedChildMenu:focus .menuEllipsis { - display: block; + display: block; } .contextmenu6 { - top: -29px; + top: -29px; } .userDefinedMenu:hover .contextmenu6 { - display: block; + display: block; } .userDefinedchildMenu:hover .menuEllipsis, .userDefinedchildMenu:focus .menuEllipsis { - display: block; + display: block; } .triggersMenu:hover .menuEllipsis, .triggersMenu:focus .menuEllipsis { - display: block; + display: block; } .triggersChildMenu:hover .menuEllipsis, .triggersChildMenu:focus .menuEllipsis { - display: block; + display: block; } .databaseId { - font-size: 14px; + font-size: 14px; } .storedUdfTriggerMenu { - padding-left: 0px; + padding-left: 0px; } .collectionstree img { - width: 16px; - height: 16px; - vertical-align: text-top; + width: 16px; + height: 16px; + vertical-align: text-top; } img.collectionsTreeCollapseExpand { - width: 10px; - height: 10px; - vertical-align: middle; - margin-bottom: 5px; + width: 10px; + height: 10px; + vertical-align: middle; + margin-bottom: 5px; } .collapsed::before { - content: "\23F5"; - margin-left: 0px; - font-size: 15px; + content: "\23F5"; + margin-left: 0px; + font-size: 15px; } .expanded::before { - content: '\23F7'; - margin-left: 0px; - font-size: 15px; + content: "\23F7"; + margin-left: 0px; + font-size: 15px; } .collectionMenuChildren { - padding-left: 42px; + padding-left: 42px; } .main-nav { - width: 100vh; - height: 40px; - background: white; - transform-origin: left top; - -webkit-transform-origin: left top; - -ms-transform-origin: left top; - transform: rotate(-90deg) translateX(-100%); - -webkit-transform: rotate(-90deg) translateX(-100%); - -ms-transform: rotate(-90deg) translateX(-100%); - border-bottom: 1px solid #CCC; + width: 100vh; + height: 40px; + background: white; + transform-origin: left top; + -webkit-transform-origin: left top; + -ms-transform-origin: left top; + transform: rotate(-90deg) translateX(-100%); + -webkit-transform: rotate(-90deg) translateX(-100%); + -ms-transform: rotate(-90deg) translateX(-100%); + border-bottom: 1px solid #ccc; } .main-nav-img { - width: 16px; - height: 16px; - margin: -32px 0 0 0; - transform: rotate(-90deg) translateX(-100%); - -webkit-transform: rotate(-90deg) translateX(-100%); - -ms-transform: rotate(-90deg) translateX(-100%); + width: 16px; + height: 16px; + margin: -32px 0 0 0; + transform: rotate(-90deg) translateX(-100%); + -webkit-transform: rotate(-90deg) translateX(-100%); + -ms-transform: rotate(-90deg) translateX(-100%); } .main-nav-img.main-nav-sub-img { - width: 16px; - height: 16px; - margin: 0px 0px 0 0; - transform: rotate(180deg) translateX(0%); - -webkit-transform: rotate(180deg) translateX(0%); - -ms-transform: rotate(180deg) translateX(0%); - position: absolute; - right: -8px; - top: 16px; + width: 16px; + height: 16px; + margin: 0px 0px 0 0; + transform: rotate(180deg) translateX(0%); + -webkit-transform: rotate(180deg) translateX(0%); + -ms-transform: rotate(180deg) translateX(0%); + position: absolute; + right: -8px; + top: 16px; } ul.nav { - margin: 0 auto; - margin-top: 0px; - margin-left: 0px; + margin: 0 auto; + margin-top: 0px; + margin-left: 0px; } .mini ul.nav li { - float: right; - line-height: 25px; - height: auto; - margin-top: 3px; + float: right; + line-height: 25px; + height: auto; + margin-top: 3px; } .spancolchildstyle { - padding: 4px; + padding: 4px; } .contextmenubutton { - float: right; - display: none; + float: right; + display: none; } -.highlight:hover>.contextmenubutton { - display: unset; +.highlight:hover > .contextmenubutton { + display: unset; } -.highlight:hover>.contextmenubutton::after { - content: "\2026"; - font-size: 12px; +.highlight:hover > .contextmenubutton::after { + content: "\2026"; + font-size: 12px; } .showEllipsis { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} \ No newline at end of file + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} diff --git a/package-lock.json b/package-lock.json index 997269344..79d3475da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5583,6 +5583,11 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" }, + "@types/lodash": { + "version": "4.14.171", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz", + "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -20618,9 +20623,9 @@ } }, "playwright": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.10.0.tgz", - "integrity": "sha512-b7SGBcCPq4W3pb4ImEDmNXtO0ZkJbZMuWiShsaNJd+rGfY/6fqwgllsAojmxGSgFmijYw7WxCoPiAIEDIH16Kw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.13.0.tgz", + "integrity": "sha512-GA5OyEeKx1v/pRcANmYncCT67Y7Y4N5zLRU5E690dn/Id10sooR5hQZmCDYsjXlutZb/1q0R3sITALnvhEjCjg==", "dev": true, "requires": { "commander": "^6.1.0", @@ -20635,7 +20640,8 @@ "proxy-from-env": "^1.1.0", "rimraf": "^3.0.2", "stack-utils": "^2.0.3", - "ws": "^7.3.1" + "ws": "^7.4.6", + "yazl": "^2.5.1" }, "dependencies": { "commander": { @@ -20667,6 +20673,12 @@ "requires": { "escape-string-regexp": "^2.0.0" } + }, + "ws": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "dev": true } } }, @@ -26157,6 +26169,15 @@ "fd-slicer": "~1.1.0" } }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3" + } + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 886b54c8f..153244680 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@octokit/rest": "17.9.2", "@phosphor/widgets": "1.9.3", "@testing-library/jest-dom": "5.11.9", + "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", "applicationinsights": "1.8.0", @@ -163,7 +164,7 @@ "mini-css-extract-plugin": "0.4.3", "monaco-editor-webpack-plugin": "1.7.0", "node-fetch": "2.6.1", - "playwright": "1.10.0", + "playwright": "1.13.0", "prettier": "2.2.1", "raw-loader": "0.5.1", "react-dev-utils": "11.0.4", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 57b65c43e..fc97d559d 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -94,7 +94,7 @@ export class Flights { public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexing = "mongoindexing"; public static readonly AutoscaleTest = "autoscaletest"; - public static readonly SchemaAnalyzer = "schemaanalyzer"; + public static readonly PartitionKeyTest = "partitionkeytest"; } export class AfecFeatures { diff --git a/src/Common/HeadersUtility.test.ts b/src/Common/HeadersUtility.test.ts index 5f46c420f..5432227fa 100644 --- a/src/Common/HeadersUtility.test.ts +++ b/src/Common/HeadersUtility.test.ts @@ -1,6 +1,6 @@ -import * as HeadersUtility from "./HeadersUtility"; -import { ExplorerSettings } from "../Shared/ExplorerSettings"; +import * as ExplorerSettings from "../Shared/ExplorerSettings"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; +import * as HeadersUtility from "./HeadersUtility"; describe("Headers Utility", () => { describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => { diff --git a/src/Common/ResourceTree.tsx b/src/Common/ResourceTreeContainer.tsx similarity index 78% rename from src/Common/ResourceTree.tsx rename to src/Common/ResourceTreeContainer.tsx index da9c1bad4..fe04f9e04 100644 --- a/src/Common/ResourceTree.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -2,17 +2,22 @@ import React, { FunctionComponent } from "react"; import arrowLeftImg from "../../images/imgarrowlefticon.svg"; import refreshImg from "../../images/refresh-cosmos.svg"; import { AuthType } from "../AuthType"; +import Explorer from "../Explorer/Explorer"; +import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree"; +import { ResourceTree } from "../Explorer/Tree/ResourceTree"; import { userContext } from "../UserContext"; -export interface ResourceTreeProps { +export interface ResourceTreeContainerProps { toggleLeftPaneExpanded: () => void; isLeftPaneExpanded: boolean; + container: Explorer; } -export const ResourceTree: FunctionComponent = ({ +export const ResourceTreeContainer: FunctionComponent = ({ toggleLeftPaneExpanded, isLeftPaneExpanded, -}: ResourceTreeProps): JSX.Element => { + container, +}: ResourceTreeContainerProps): JSX.Element => { return (
{/* Collections Window - - Start */} @@ -48,9 +53,11 @@ export const ResourceTree: FunctionComponent = ({
{userContext.authType === AuthType.ResourceToken ? ( -
- ) : ( + + ) : userContext.features.enableKOResourceTree ? (
+ ) : ( + )}
{/* Collections Window - End */} diff --git a/src/Common/dataAccess/createCollection.test.ts b/src/Common/dataAccess/createCollection.test.ts index ce04404a6..2f6bf63e4 100644 --- a/src/Common/dataAccess/createCollection.test.ts +++ b/src/Common/dataAccess/createCollection.test.ts @@ -1,7 +1,10 @@ jest.mock("../../Utils/arm/request"); jest.mock("../CosmosClient"); +import ko from "knockout"; import { AuthType } from "../../AuthType"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; +import { Database } from "../../Contracts/ViewModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { updateUserContext } from "../../UserContext"; import { armRequest } from "../../Utils/arm/request"; import { client } from "../CosmosClient"; @@ -23,6 +26,15 @@ describe("createCollection", () => { } as DatabaseAccount, apiType: "SQL", }); + useDatabases.setState({ + databases: [ + { + id: ko.observable("testDatabase"), + loadCollections: () => undefined, + collections: ko.observableArray([]), + } as Database, + ], + }); }); it("should call ARM if logged in with AAD", async () => { diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 6d7798dd8..791b29fcc 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -4,20 +4,16 @@ import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/Contai import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; -import { - createUpdateCassandraTable, - getCassandraTable, -} from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; -import { createUpdateGremlinGraph, getGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; -import { - createUpdateMongoDBCollection, - getMongoDBCollection, -} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; -import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; -import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; +import { getCollectionName } from "../../Utils/APITypeUtils"; +import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; @@ -59,6 +55,16 @@ export const createCollection = async (params: DataModels.CreateCollectionParams }; const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise => { + if (!params.createNewDatabase) { + const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId); + if (!isValid) { + const collectionName = getCollectionName().toLocaleLowerCase(); + throw new Error( + `Create ${collectionName} failed: ${collectionName} with id ${params.collectionId} already exists` + ); + } + } + const { apiType } = userContext; switch (apiType) { case "SQL": @@ -77,23 +83,6 @@ const createCollectionWithARM = async (params: DataModels.CreateCollectionParams }; const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getSqlContainer( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create container failed: container with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.SqlContainerResource = { id: params.collectionId, @@ -131,23 +120,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise => { const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }]; - try { - const getResponse = await getMongoDBCollection( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.MongoDBCollectionResource = { id: params.collectionId, @@ -189,23 +161,6 @@ const createMongoCollection = async (params: DataModels.CreateCollectionParams): }; const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getCassandraTable( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create table failed: table with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.CassandraTableResource = { id: params.collectionId, @@ -233,23 +188,6 @@ const createCassandraTable = async (params: DataModels.CreateCollectionParams): }; const createGraph = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getGremlinGraph( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.GremlinGraphResource = { id: params.collectionId, @@ -284,22 +222,6 @@ const createGraph = async (params: DataModels.CreateCollectionParams): Promise => { - try { - const getResponse = await getTable( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.collectionId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create table failed: table with id ${params.collectionId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params); const resource: ARMTypes.TableResource = { id: params.collectionId, diff --git a/src/Common/dataAccess/createDatabase.ts b/src/Common/dataAccess/createDatabase.ts index 7d13871f6..2467b7975 100644 --- a/src/Common/dataAccess/createDatabase.ts +++ b/src/Common/dataAccess/createDatabase.ts @@ -2,20 +2,13 @@ import { DatabaseResponse } from "@azure/cosmos"; import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest"; import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; +import { useDatabases } from "../../Explorer/useDatabases"; import { userContext } from "../../UserContext"; -import { - createUpdateCassandraKeyspace, - getCassandraKeyspace, -} from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; -import { - createUpdateGremlinDatabase, - getGremlinDatabase, -} from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; -import { - createUpdateMongoDBDatabase, - getMongoDBDatabase, -} from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; -import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { getDatabaseName } from "../../Utils/APITypeUtils"; +import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; +import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; +import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; +import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { CassandraKeyspaceCreateUpdateParameters, CreateUpdateOptions, @@ -48,6 +41,11 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P } async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise { + if (!useDatabases.getState().validateDatabaseId(params.databaseId)) { + const databaseName = getDatabaseName().toLocaleLowerCase(); + throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`); + } + const { apiType } = userContext; switch (apiType) { @@ -65,22 +63,6 @@ async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): P } async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getSqlDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: SqlDatabaseCreateUpdateParameters = { properties: { @@ -101,22 +83,6 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi } async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getMongoDBDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: MongoDBDatabaseCreateUpdateParameters = { properties: { @@ -137,22 +103,6 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro } async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getCassandraKeyspace( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: CassandraKeyspaceCreateUpdateParameters = { properties: { @@ -173,22 +123,6 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): } async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise { - try { - const getResponse = await getGremlinDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId - ); - if (getResponse?.properties?.resource) { - throw new Error(`Create database failed: database with id ${params.databaseId} already exists`); - } - } catch (error) { - if (error.code !== "NotFound") { - throw error; - } - } - const options: CreateUpdateOptions = constructRpOptions(params); const rpPayload: GremlinDatabaseCreateUpdateParameters = { properties: { diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index e83a4e9ec..289e650cb 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -27,7 +27,6 @@ export interface ConfigContext { hostedExplorerURL: string; armAPIVersion?: string; allowedJunoOrigins: string[]; - enableSchemaAnalyzer: boolean; msalRedirectURI?: string; } @@ -63,7 +62,6 @@ let configContext: Readonly = { "https://tools-staging.cosmos.azure.com", "https://localhost", ], - enableSchemaAnalyzer: false, }; export function resetConfigContext(): void { diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index e16ebdf73..a4933c688 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -9,6 +9,7 @@ export interface DatabaseAccount { export interface DatabaseAccountExtendedProperties { documentEndpoint?: string; + disableLocalAuth?: boolean; tableEndpoint?: string; gremlinEndpoint?: string; cassandraEndpoint?: string; diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index ece39b07d..70d02e5aa 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -109,7 +109,7 @@ export const createCollectionContextMenuButton = ( iconSrc: AddUdfIcon, onClick: () => { const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); - selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined); + selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); }, label: "New UDF", }); diff --git a/src/Explorer/Controls/Accordion/AccordionComponent.tsx b/src/Explorer/Controls/Accordion/AccordionComponent.tsx index 06563c4a3..f2b5c1b45 100644 --- a/src/Explorer/Controls/Accordion/AccordionComponent.tsx +++ b/src/Explorer/Controls/Accordion/AccordionComponent.tsx @@ -8,7 +8,9 @@ import TriangleDownIcon from "../../../../images/Triangle-down.svg"; import TriangleRightIcon from "../../../../images/Triangle-right.svg"; import * as Constants from "../../../Common/Constants"; -export interface AccordionComponentProps {} +export interface AccordionComponentProps { + children: React.ReactNode; +} export class AccordionComponent extends React.Component { public render(): JSX.Element { @@ -78,7 +80,7 @@ export class AccordionItemComponent extends React.Component): void => { + private onHeaderClick = (): void => { this.setState({ isExpanded: !this.state.isExpanded }); }; diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index f4694cdbc..2c0f6f283 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -121,8 +121,7 @@ export class CommandButtonComponent extends React.Component { + queryEditorModel.onDidChangeContent(() => { const queryEditorModel = this.editor.getModel(); this.props.onContentChanged(queryEditorModel.getValue()); }); diff --git a/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx b/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx index 29e1a2900..d6b28775d 100644 --- a/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx +++ b/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx @@ -56,7 +56,7 @@ export class GitHubReposComponent extends React.Component -
{content}
+
{content}
{!this.props.showAuthorizeAccess && ( <>
diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx index 627041b38..e991fe05c 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx @@ -1,154 +1,90 @@ +import { shallow } from "enzyme"; +import React from "react"; import * as DataModels from "../../../Contracts/DataModels"; -import { NotebookTerminalComponent } from "./NotebookTerminalComponent"; +import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent"; -const createTestDatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: null, - documentEndpoint: "https://testDocumentEndpoint.azure.com/", - gremlinEndpoint: null, - tableEndpoint: null, - }, - type: "testType", - }; +const testAccount: DataModels.DatabaseAccount = { + id: "id", + kind: "kind", + location: "location", + name: "name", + properties: { + documentEndpoint: "https://testDocumentEndpoint.azure.com/", + }, + type: "type", }; -const createTestMongo32DatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: null, - documentEndpoint: "https://testDocumentEndpoint.azure.com/", - gremlinEndpoint: null, - tableEndpoint: null, - }, - type: "testType", - }; +const testMongo32Account: DataModels.DatabaseAccount = { + ...testAccount, }; -const createTestMongo36DatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: null, - documentEndpoint: "https://testDocumentEndpoint.azure.com/", - gremlinEndpoint: null, - tableEndpoint: null, - mongoEndpoint: "https://testMongoEndpoint.azure.com/", - }, - type: "testType", - }; +const testMongo36Account: DataModels.DatabaseAccount = { + ...testAccount, + properties: { + mongoEndpoint: "https://testMongoEndpoint.azure.com/", + }, }; -const createTestCassandraDatabaseAccount = (): DataModels.DatabaseAccount => { - return { - id: "testId", - kind: "testKind", - location: "testLocation", - name: "testName", - properties: { - cassandraEndpoint: "https://testCassandraEndpoint.azure.com/", - documentEndpoint: null, - gremlinEndpoint: null, - tableEndpoint: null, - }, - type: "testType", - }; +const testCassandraAccount: DataModels.DatabaseAccount = { + ...testAccount, + properties: { + cassandraEndpoint: "https://testCassandraEndpoint.azure.com/", + }, }; -const createTerminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/", - }, - databaseAccount: createTestDatabaseAccount(), - }); +const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { + authToken: "authToken", + notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com", }; -const createMongo32Terminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo", - }, - databaseAccount: createTestMongo32DatabaseAccount(), - }); +const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { + authToken: "authToken", + notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo", }; -const createMongo36Terminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo", - }, - databaseAccount: createTestMongo36DatabaseAccount(), - }); -}; - -const createCassandraTerminal = (): NotebookTerminalComponent => { - return new NotebookTerminalComponent({ - notebookServerInfo: { - authToken: "testAuthToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra", - }, - databaseAccount: createTestCassandraDatabaseAccount(), - }); +const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { + authToken: "authToken", + notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra", }; describe("NotebookTerminalComponent", () => { - it("getTerminalParams: Test for terminal", () => { - const terminal: NotebookTerminalComponent = createTerminal(); - const params: Map = terminal.getTerminalParams(); + it("renders terminal", () => { + const props: NotebookTerminalComponentProps = { + databaseAccount: testAccount, + notebookServerInfo: testNotebookServerInfo, + }; - expect(params).toEqual( - new Map([["terminal", "true"]]) - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); - it("getTerminalParams: Test for Mongo 3.2 terminal", () => { - const terminal: NotebookTerminalComponent = createMongo32Terminal(); - const params: Map = terminal.getTerminalParams(); + it("renders mongo 3.2 shell", () => { + const props: NotebookTerminalComponentProps = { + databaseAccount: testMongo32Account, + notebookServerInfo: testMongoNotebookServerInfo, + }; - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host], - ]) - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); - it("getTerminalParams: Test for Mongo 3.6 terminal", () => { - const terminal: NotebookTerminalComponent = createMongo36Terminal(); - const params: Map = terminal.getTerminalParams(); + it("renders mongo 3.6 shell", () => { + const props: NotebookTerminalComponentProps = { + databaseAccount: testMongo36Account, + notebookServerInfo: testMongoNotebookServerInfo, + }; - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host], - ]) - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); - it("getTerminalParams: Test for Cassandra terminal", () => { - const terminal: NotebookTerminalComponent = createCassandraTerminal(); - const params: Map = terminal.getTerminalParams(); + it("renders cassandra shell", () => { + const props: NotebookTerminalComponentProps = { + databaseAccount: testCassandraAccount, + notebookServerInfo: testCassandraNotebookServerInfo, + }; - expect(params).toEqual( - new Map([ - ["terminal", "true"], - ["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host], - ]) - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx index a7b51e896..637f24192 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx @@ -2,12 +2,12 @@ * Wrapper around Notebook server terminal */ +import postRobot from "post-robot"; import * as React from "react"; import * as DataModels from "../../../Contracts/DataModels"; -import * as StringUtils from "../../../Utils/StringUtils"; +import { TerminalProps } from "../../../Terminal/TerminalProps"; import { userContext } from "../../../UserContext"; -import { TerminalQueryParams } from "../../../Common/Constants"; -import { handleError } from "../../../Common/ErrorHandlingUtils"; +import * as StringUtils from "../../../Utils/StringUtils"; export interface NotebookTerminalComponentProps { notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; @@ -15,79 +15,69 @@ export interface NotebookTerminalComponentProps { } export class NotebookTerminalComponent extends React.Component { + private terminalWindow: Window; + constructor(props: NotebookTerminalComponentProps) { super(props); } + componentDidMount(): void { + this.sendPropsToTerminalFrame(); + } + public render(): JSX.Element { return (
+ )} + + {navigationSelection === "explorer" && ( + + )} + + ); +}; + +ReactDOM.render(, document.getElementById("root")); diff --git a/src/Localization/en/SqlX.json b/src/Localization/en/SqlX.json index 58f6e89a6..9c0ac667e 100644 --- a/src/Localization/en/SqlX.json +++ b/src/Localization/en/SqlX.json @@ -40,7 +40,7 @@ "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", - "Cost": "Cost", + "ApproximateCost": "Approximate Cost Per Hour", "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", "ConnectionString": "Connection String", "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", diff --git a/src/Main.tsx b/src/Main.tsx index c85a39f10..80d008b17 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -26,7 +26,7 @@ import "../less/TableStyles/fulldatatables.less"; import "../less/TableStyles/queryBuilder.less"; import "../less/tree.less"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; -import { ResourceTree } from "./Common/ResourceTree"; +import { ResourceTreeContainer } from "./Common/ResourceTreeContainer"; import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import { Dialog } from "./Explorer/Controls/Dialog"; @@ -84,7 +84,11 @@ const App: React.FunctionComponent = () => {
{/* Collections Tree Expanded - Start */} - + {/* Collections Tree Expanded - End */} {/* Collections Tree Collapsed - Start */} = ({ data: account, }))} onChange={(_, option) => { - setSelectedAccountName(String(option.key)); + setSelectedAccountName(String(option?.key)); dismissMenu(); }} defaultSelectedKey={selectedAccount?.name} diff --git a/src/Platform/Hosted/Components/SwitchSubscription.tsx b/src/Platform/Hosted/Components/SwitchSubscription.tsx index 9ec98fc27..c784c5f5b 100644 --- a/src/Platform/Hosted/Components/SwitchSubscription.tsx +++ b/src/Platform/Hosted/Components/SwitchSubscription.tsx @@ -26,7 +26,7 @@ export const SwitchSubscription: FunctionComponent = ({ }; })} onChange={(_, option) => { - setSelectedSubscriptionId(String(option.key)); + setSelectedSubscriptionId(String(option?.key)); }} defaultSelectedKey={selectedSubscription?.subscriptionId} placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"} diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts index 8762171e7..bd1447891 100644 --- a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts +++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts @@ -2,8 +2,8 @@ import * as DataModels from "../../../Contracts/DataModels"; import { parseConnectionString } from "./ConnectionStringParser"; describe("ConnectionStringParser", () => { - const mockAccountName: string = "Test"; - const mockMasterKey: string = "some-key"; + const mockAccountName = "Test"; + const mockMasterKey = "some-key"; it("should parse a valid sql account connection string", () => { const metadata = parseConnectionString( diff --git a/src/Platform/Hosted/HostedUtils.ts b/src/Platform/Hosted/HostedUtils.ts index 8cc59daa4..f39e318b1 100644 --- a/src/Platform/Hosted/HostedUtils.ts +++ b/src/Platform/Hosted/HostedUtils.ts @@ -40,7 +40,7 @@ export function getDatabaseAccountKindFromExperience(apiExperience: typeof userC return AccountKind.GlobalDocumentDB; } -export function extractMasterKeyfromConnectionString(connectionString: string): string { +export function extractMasterKeyfromConnectionString(connectionString: string): string | undefined { // Only Gremlin uses the actual master key for connection to cosmos const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$"); return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined; diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 95e3695df..673d0c255 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -8,13 +8,14 @@ export type Features = { readonly enableReactPane: boolean; readonly enableRightPanelV2: boolean; readonly enableSchema: boolean; - enableSchemaAnalyzer: boolean; autoscaleDefault: boolean; + partitionKeyDefault: boolean; readonly enableSDKoperations: boolean; readonly enableSpark: boolean; readonly enableTtl: boolean; readonly executeSproc: boolean; readonly enableAadDataPlane: boolean; + readonly enableKOResourceTree: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; readonly livyEndpoint?: string; @@ -53,10 +54,10 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableReactPane: "true" === get("enablereactpane"), enableRightPanelV2: "true" === get("enablerightpanelv2"), enableSchema: "true" === get("enableschema"), - enableSchemaAnalyzer: "true" === get("enableschemaanalyzer"), enableSDKoperations: "true" === get("enablesdkoperations"), enableSpark: "true" === get("enablespark"), enableTtl: "true" === get("enablettl"), + enableKOResourceTree: "true" === get("enablekoresourcetree"), executeSproc: "true" === get("dataexplorerexecutesproc"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), junoEndpoint: get("junoendpoint"), @@ -70,5 +71,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear showMinRUSurvey: "true" === get("showminrusurvey"), ttl90Days: "true" === get("ttl90days"), autoscaleDefault: "true" === get("autoscaledefault"), + partitionKeyDefault: "true" === get("partitionkeytest"), }; } diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index 365422c57..61080763e 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -4,7 +4,12 @@ import { armRequestWithoutPolling } from "../../Utils/arm/request"; import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; import { RefreshResult } from "../SelfServeTypes"; import SqlX from "./SqlX"; -import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes"; +import { + FetchPricesResponse, + RegionsResponse, + SqlxServiceResource, + UpdateDedicatedGatewayRequestParameters, +} from "./SqlxTypes"; const apiVersion = "2021-04-01-preview"; @@ -128,3 +133,67 @@ export const refreshDedicatedGatewayProvisioning = async (): Promise { + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`; +}; + +export const getReadRegions = async (): Promise> => { + try { + const readRegions = new Array(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name), + method: "GET", + apiVersion: "2021-04-01-preview", + }); + + if (response.result.location !== undefined) { + readRegions.push(response.result.location.replace(" ", "").toLowerCase()); + } else { + for (const location of response.result.locations) { + readRegions.push(location.locationName.replace(" ", "").toLowerCase()); + } + } + return readRegions; + } catch (err) { + return new Array(); + } +}; + +const getFetchPricesPathForRegion = (subscriptionId: string): string => { + return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; +}; + +export const getPriceMap = async (regions: Array): Promise>> => { + try { + const priceMap = new Map>(); + + for (const region of regions) { + const regionPriceMap = new Map(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getFetchPricesPathForRegion(userContext.subscriptionId), + method: "POST", + apiVersion: "2020-01-01-preview", + queryParams: { + filter: + "armRegionName eq '" + + region + + "' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'", + }, + }); + + for (const item of response.result.Items) { + regionPriceMap.set(item.skuName, item.retailPrice); + } + priceMap.set(region, regionPriceMap); + } + + return priceMap; + } catch (err) { + return undefined; + } +}; diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index c6a431ede..ca1177fa3 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -16,11 +16,13 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils"; import { deleteDedicatedGatewayResource, getCurrentProvisioningState, + getPriceMap, + getReadRegions, refreshDedicatedGatewayProvisioning, updateDedicatedGatewayResource, } from "./SqlX.rp"; -const costPerHourValue: Description = { +const costPerHourDefaultValue: Description = { textTKey: "CostText", type: DescriptionType.Text, link: { @@ -53,7 +55,10 @@ const CosmosD16s = "Cosmos.D16s"; const onSKUChange = (newValue: InputType, currentValues: Map): Map => { currentValues.set("sku", { value: newValue }); - currentValues.set("costPerHour", { value: costPerHourValue }); + currentValues.set("costPerHour", { + value: calculateCost(newValue as string, currentValues.get("instances").value as number), + }); + return currentValues; }; @@ -79,6 +84,11 @@ const onNumberOfInstancesChange = ( } else { currentValues.set("warningBanner", undefined); } + + currentValues.set("costPerHour", { + value: calculateCost(currentValues.get("sku").value as string, newValue as number), + }); + return currentValues; }; @@ -111,6 +121,11 @@ const onEnableDedicatedGatewayChange = ( } as Description, hidden: false, }); + + currentValues.set("costPerHour", { + value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number), + hidden: false, + }); } else { currentValues.set("warningBanner", { value: { @@ -122,6 +137,8 @@ const onEnableDedicatedGatewayChange = ( } as Description, hidden: false, }); + + currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true }); } const sku = currentValues.get("sku"); const instances = currentValues.get("instances"); @@ -137,7 +154,6 @@ const onEnableDedicatedGatewayChange = ( disabled: dedicatedGatewayOriginallyEnabled, }); - currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes }); currentValues.set("connectionString", { value: connectionStringValue, hidden: !newValue || !dedicatedGatewayOriginallyEnabled, @@ -177,6 +193,40 @@ const NumberOfInstancesDropdownInfo: Info = { }, }; +const ApproximateCostDropDownInfo: Info = { + messageTKey: "CostText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", + textTKey: "DedicatedGatewayPricing", + }, +}; + +let priceMap: Map>; +let regions: Array; + +const calculateCost = (skuName: string, instanceCount: number): Description => { + try { + let costPerHour = 0; + for (const region of regions) { + const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", "")); + if (incrementalCost === undefined) { + throw new Error("Value not found in map."); + } + costPerHour += incrementalCost; + } + + costPerHour *= instanceCount; + costPerHour = Math.round(costPerHour * 100) / 100; + + return { + textTKey: `${costPerHour} USD`, + type: DescriptionType.Text, + }; + } catch (err) { + return costPerHourDefaultValue; + } +}; + @IsDisplayable() @RefreshOptions({ retryIntervalInMs: 20000 }) export default class SqlX extends SelfServeBaseClass { @@ -274,12 +324,15 @@ export default class SqlX extends SelfServeBaseClass { hidden: true, }); + regions = await getReadRegions(); + priceMap = await getPriceMap(regions); + const response = await getCurrentProvisioningState(); if (response.status && response.status !== "Deleting") { defaults.set("enableDedicatedGateway", { value: true }); defaults.set("sku", { value: response.sku, disabled: true }); defaults.set("instances", { value: response.instances, disabled: false }); - defaults.set("costPerHour", { value: costPerHourValue }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); defaults.set("connectionString", { value: connectionStringValue, hidden: false, @@ -338,8 +391,9 @@ export default class SqlX extends SelfServeBaseClass { }) instances: number; + @PropertyInfo(ApproximateCostDropDownInfo) @Values({ - labelTKey: "Cost", + labelTKey: "ApproximateCost", isDynamicDescription: true, }) costPerHour: string; diff --git a/src/SelfServe/SqlX/SqlxTypes.ts b/src/SelfServe/SqlX/SqlxTypes.ts index 70557f4f4..a150ccbb1 100644 --- a/src/SelfServe/SqlX/SqlxTypes.ts +++ b/src/SelfServe/SqlX/SqlxTypes.ts @@ -29,3 +29,23 @@ export type UpdateDedicatedGatewayRequestProperties = { instanceCount: number; serviceType: string; }; + +export type FetchPricesResponse = { + Items: Array; + NextPageLink: string | undefined; + Count: number; +}; + +export type PriceItem = { + retailPrice: number; + skuName: string; +}; + +export type RegionsResponse = { + locations: Array; + location: string; +}; + +export type RegionItem = { + locationName: string; +}; diff --git a/src/Shared/DefaultExperienceUtility.test.ts b/src/Shared/DefaultExperienceUtility.test.ts index c72b46823..2d23af40a 100644 --- a/src/Shared/DefaultExperienceUtility.test.ts +++ b/src/Shared/DefaultExperienceUtility.test.ts @@ -35,7 +35,7 @@ describe("Default Experience Utility", () => { }); describe("getApiKindFromDefaultExperience()", () => { - function runScenario(defaultExperience: typeof userContext.apiType, expectedApiKind: number): void { + function runScenario(defaultExperience: typeof userContext.apiType | null, expectedApiKind: number): void { const resolvedApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(defaultExperience); expect(resolvedApiKind).toEqual(expectedApiKind); } diff --git a/src/Shared/ExplorerSettings.ts b/src/Shared/ExplorerSettings.ts index b697dac83..3c6f9e3d2 100644 --- a/src/Shared/ExplorerSettings.ts +++ b/src/Shared/ExplorerSettings.ts @@ -1,22 +1,17 @@ import * as Constants from "../Common/Constants"; import { LocalStorageUtility, StorageKey } from "./StorageUtility"; -export class ExplorerSettings { - public static createDefaultSettings() { - LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, Constants.Queries.itemsPerPage); - LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage); - LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true"); - LocalStorageUtility.setEntryNumber( - StorageKey.MaxDegreeOfParellism, - Constants.Queries.DefaultMaxDegreeOfParallelism - ); - } +export const createDefaultSettings = () => { + LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, Constants.Queries.itemsPerPage); + LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage); + LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true"); + LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism); +}; - public static hasSettingsDefined(): boolean { - return ( - LocalStorageUtility.hasItem(StorageKey.ActualItemPerPage) && - LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled) && - LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism) - ); - } -} +export const hasSettingsDefined = (): boolean => { + return ( + LocalStorageUtility.hasItem(StorageKey.ActualItemPerPage) && + LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled) && + LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism) + ); +}; diff --git a/src/Shared/LocalStorageUtility.ts b/src/Shared/LocalStorageUtility.ts new file mode 100644 index 000000000..9fc2f4f7c --- /dev/null +++ b/src/Shared/LocalStorageUtility.ts @@ -0,0 +1,22 @@ +import { StorageKey } from "./StorageUtility"; +import * as StringUtility from "./StringUtility"; + +export const hasItem = (key: StorageKey): boolean => !!localStorage.getItem(StorageKey[key]); + +export const getEntryString = (key: StorageKey): string | null => localStorage.getItem(StorageKey[key]); + +export const getEntryNumber = (key: StorageKey): number => + StringUtility.toNumber(localStorage.getItem(StorageKey[key])); + +export const getEntryBoolean = (key: StorageKey): boolean => + StringUtility.toBoolean(localStorage.getItem(StorageKey[key])); + +export const setEntryString = (key: StorageKey, value: string): void => localStorage.setItem(StorageKey[key], value); + +export const removeEntry = (key: StorageKey): void => localStorage.removeItem(StorageKey[key]); + +export const setEntryNumber = (key: StorageKey, value: number): void => + localStorage.setItem(StorageKey[key], value.toString()); + +export const setEntryBoolean = (key: StorageKey, value: boolean): void => + localStorage.setItem(StorageKey[key], value.toString()); diff --git a/src/Shared/PriceEstimateCalculator.ts b/src/Shared/PriceEstimateCalculator.ts index 2e897ea66..a7f08d230 100644 --- a/src/Shared/PriceEstimateCalculator.ts +++ b/src/Shared/PriceEstimateCalculator.ts @@ -2,26 +2,26 @@ import * as Constants from "./Constants"; export function computeRUUsagePrice(serverId: string, requestUnits: number): string { if (serverId === "mooncake") { - let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; + const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; } - let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; + const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; } export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string { if (serverId === "mooncake") { - let storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerGB; + const storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerGB; return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; } - let storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerGB; + const storageCharge = storageUsedRoundUpToGB * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerGB; return calculateEstimateNumber(storageCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; } export function computeDisplayUsageString(usageInKB: number): string { - let usageInMB = usageInKB / 1024, + const usageInMB = usageInKB / 1024, usageInGB = usageInMB / 1024, displayUsageString = usageInGB > 0.1 @@ -33,7 +33,7 @@ export function computeDisplayUsageString(usageInKB: number): string { } export function usageInGB(usageInKB: number): number { - let usageInMB = usageInKB / 1024, + const usageInMB = usageInKB / 1024, usageInGB = usageInMB / 1024; return Math.ceil(usageInGB); } diff --git a/src/Shared/SessionStorageUtility.ts b/src/Shared/SessionStorageUtility.ts new file mode 100644 index 000000000..f3fb4abae --- /dev/null +++ b/src/Shared/SessionStorageUtility.ts @@ -0,0 +1,20 @@ +import { StorageKey } from "./StorageUtility"; +import * as StringUtility from "./StringUtility"; + +export const hasItem = (key: StorageKey): boolean => !!sessionStorage.getItem(StorageKey[key]); + +export const getEntryString = (key: StorageKey): string | null => sessionStorage.getItem(StorageKey[key]); + +export const getEntryNumber = (key: StorageKey): number => + StringUtility.toNumber(sessionStorage.getItem(StorageKey[key])); + +export const getEntry = (key: string): string | null => sessionStorage.getItem(key); + +export const removeEntry = (key: StorageKey): void => sessionStorage.removeItem(StorageKey[key]); + +export const setEntryString = (key: StorageKey, value: string): void => sessionStorage.setItem(StorageKey[key], value); + +export const setEntry = (key: string, value: string): void => sessionStorage.setItem(key, value); + +export const setEntryNumber = (key: StorageKey, value: number): void => + sessionStorage.setItem(StorageKey[key], value.toString()); diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index adf271cec..9c16467d2 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -1,73 +1,7 @@ -import * as StringUtility from "./StringUtility"; - -export class LocalStorageUtility { - public static hasItem(key: StorageKey): boolean { - return !!localStorage.getItem(StorageKey[key]); - } - - public static getEntryString(key: StorageKey): string | null { - return localStorage.getItem(StorageKey[key]); - } - - public static getEntryNumber(key: StorageKey): number { - return StringUtility.toNumber(localStorage.getItem(StorageKey[key])); - } - - public static getEntryBoolean(key: StorageKey): boolean { - return StringUtility.toBoolean(localStorage.getItem(StorageKey[key])); - } - - public static setEntryString(key: StorageKey, value: string): void { - localStorage.setItem(StorageKey[key], value); - } - - public static removeEntry(key: StorageKey): void { - return localStorage.removeItem(StorageKey[key]); - } - - public static setEntryNumber(key: StorageKey, value: number): void { - localStorage.setItem(StorageKey[key], value.toString()); - } - - public static setEntryBoolean(key: StorageKey, value: boolean): void { - localStorage.setItem(StorageKey[key], value.toString()); - } -} - -export class SessionStorageUtility { - public static hasItem(key: StorageKey): boolean { - return !!sessionStorage.getItem(StorageKey[key]); - } - - public static getEntryString(key: StorageKey): string | null { - return sessionStorage.getItem(StorageKey[key]); - } - - public static getEntryNumber(key: StorageKey): number { - return StringUtility.toNumber(sessionStorage.getItem(StorageKey[key])); - } - - public static getEntry(key: string): string | null { - return sessionStorage.getItem(key); - } - - public static removeEntry(key: StorageKey): void { - return sessionStorage.removeItem(StorageKey[key]); - } - - public static setEntryString(key: StorageKey, value: string): void { - sessionStorage.setItem(StorageKey[key], value); - } - - public static setEntry(key: string, value: string): void { - sessionStorage.setItem(key, value); - } - - public static setEntryNumber(key: StorageKey, value: number): void { - sessionStorage.setItem(StorageKey[key], value.toString()); - } -} +import * as LocalStorageUtility from "./LocalStorageUtility"; +import * as SessionStorageUtility from "./SessionStorageUtility"; +export { LocalStorageUtility, SessionStorageUtility }; export enum StorageKey { ActualItemPerPage, CustomItemPerPage, diff --git a/src/Terminal/TerminalProps.ts b/src/Terminal/TerminalProps.ts new file mode 100644 index 000000000..4fe3c539c --- /dev/null +++ b/src/Terminal/TerminalProps.ts @@ -0,0 +1,13 @@ +import { AuthType } from "../AuthType"; +import * as DataModels from "../Contracts/DataModels"; +import { ApiType } from "../UserContext"; + +export interface TerminalProps { + authToken: string; + notebookServerEndpoint: string; + terminalEndpoint: string; + databaseAccount: DataModels.DatabaseAccount; + authType: AuthType; + apiType: ApiType; + subscriptionId: string; +} diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index 02524eaad..dae3059b5 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -1,43 +1,36 @@ import { ServerConnection } from "@jupyterlab/services"; import "@jupyterlab/terminal/style/index.css"; -import { HttpHeaders, TerminalQueryParams } from "../Common/Constants"; +import postRobot from "post-robot"; +import { HttpHeaders } from "../Common/Constants"; import { Action } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../UserContext"; import "./index.css"; import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; +import { TerminalProps } from "./TerminalProps"; -const getUrlVars = (): { [key: string]: string } => { - const vars: { [key: string]: string } = {}; - window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (_m, key, value): string => { - vars[key] = decodeURIComponent(value); - return value; - }); - return vars; -}; - -const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => { +const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => { let body: BodyInit | undefined; let headers: HeadersInit | undefined; - if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) { + if (props.terminalEndpoint) { body = JSON.stringify({ - endpoint: urlVars[TerminalQueryParams.TerminalEndpoint], + endpoint: props.terminalEndpoint, }); headers = { [HttpHeaders.contentType]: "application/json", }; } - const server = urlVars[TerminalQueryParams.Server]; + const server = props.notebookServerEndpoint; let options: Partial = { baseUrl: server, init: { body, headers }, fetch: window.parent.fetch, }; - if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) { + if (props.authToken) { options = { baseUrl: server, - token: urlVars[TerminalQueryParams.Token], + token: props.authToken, appendToken: true, init: { body, headers }, fetch: window.parent.fetch, @@ -47,30 +40,41 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect return ServerConnection.makeSettings(options); }; -const main = async (): Promise => { - const urlVars = getUrlVars(); - - // Initialize userContext. Currently only subscriptionId is required by TelemetryProcessor +const initTerminal = async (props: TerminalProps) => { + // Initialize userContext (only properties which are needed by TelemetryProcessor) updateUserContext({ - subscriptionId: urlVars[TerminalQueryParams.SubscriptionId], + subscriptionId: props.subscriptionId, + apiType: props.apiType, + authType: props.authType, + databaseAccount: props.databaseAccount, }); - const serverSettings = createServerSettings(urlVars); - + const serverSettings = createServerSettings(props); const data = { baseUrl: serverSettings.baseUrl }; const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); try { - if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) { - await JupyterLabAppFactory.createTerminalApp(serverSettings); - } else { - throw new Error("Only terminal is supported"); - } - + await JupyterLabAppFactory.createTerminalApp(serverSettings); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); } catch (error) { TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); } }; +const main = async (): Promise => { + postRobot.on( + "props", + { + window: window.parent, + domain: window.location.origin, + }, + async (event) => { + // Typescript definition for event is wrong. So read props by casting to + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const props = (event as any).data as TerminalProps; + await initTerminal(props); + } + ); +}; + window.addEventListener("load", main); diff --git a/src/TokenProviders/PortalTokenProvider.ts b/src/TokenProviders/PortalTokenProvider.ts deleted file mode 100644 index c7b1480ad..000000000 --- a/src/TokenProviders/PortalTokenProvider.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as ViewModels from "../Contracts/ViewModels"; -import { userContext } from "../UserContext"; - -export class PortalTokenProvider implements ViewModels.TokenProvider { - constructor() {} - - public async getAuthHeader(): Promise { - const bearerToken = userContext.authorizationToken; - let fetchHeaders = new Headers(); - fetchHeaders.append("authorization", bearerToken); - return fetchHeaders; - } -} diff --git a/src/TokenProviders/TokenProviderFactory.ts b/src/TokenProviders/TokenProviderFactory.ts deleted file mode 100644 index 342661c3b..000000000 --- a/src/TokenProviders/TokenProviderFactory.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { configContext, Platform } from "../ConfigContext"; -import * as ViewModels from "../Contracts/ViewModels"; -import { PortalTokenProvider } from "./PortalTokenProvider"; - -export class TokenProviderFactory { - private constructor() {} - - public static create(): ViewModels.TokenProvider { - const platformType = configContext.platform; - switch (platformType) { - case Platform.Portal: - case Platform.Hosted: - return new PortalTokenProvider(); - case Platform.Emulator: - default: - // should never get into this state - throw new Error(`Unknown platform ${platformType}`); - } - } -} diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index 040766219..fc2de8149 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -1,7 +1,12 @@ import * as Constants from "../Common/Constants"; import { userContext } from "../UserContext"; -export const isCapabilityEnabled = (capabilityName: string): boolean => - userContext.databaseAccount?.properties?.capabilities?.some((capability) => capability.name === capabilityName); +export const isCapabilityEnabled = (capabilityName: string): boolean => { + const { databaseAccount } = userContext; + if (databaseAccount && databaseAccount.properties && databaseAccount.properties.capabilities) { + return databaseAccount.properties.capabilities.some((capability) => capability.name === capabilityName); + } + return false; +}; export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless); diff --git a/src/Utils/PricingUtils.test.ts b/src/Utils/PricingUtils.test.ts index 582c40a37..de4efa90c 100644 --- a/src/Utils/PricingUtils.test.ts +++ b/src/Utils/PricingUtils.test.ts @@ -18,7 +18,7 @@ describe("PricingUtils Tests", () => { }); it("should return false if passed number is not number", () => { - const value = PricingUtils.isLargerThanDefaultMinRU(null); + const value = PricingUtils.isLargerThanDefaultMinRU(undefined); expect(value).toBe(false); }); }); @@ -28,7 +28,7 @@ describe("PricingUtils Tests", () => { const value = PricingUtils.computeRUUsagePriceHourly({ serverId: "default", requestUnits: 1, - numberOfRegions: null, + numberOfRegions: undefined, multimasterEnabled: false, isAutoscale: false, }); @@ -38,7 +38,7 @@ describe("PricingUtils Tests", () => { const value = PricingUtils.computeRUUsagePriceHourly({ serverId: "default", requestUnits: 1, - numberOfRegions: null, + numberOfRegions: undefined, multimasterEnabled: false, isAutoscale: true, }); @@ -264,11 +264,6 @@ describe("PricingUtils Tests", () => { describe("getRegionMultiplier()", () => { describe("without multimaster", () => { - it("should return 0 for null", () => { - const value = PricingUtils.getRegionMultiplier(null, false); - expect(value).toBe(0); - }); - it("should return 0 for undefined", () => { const value = PricingUtils.getRegionMultiplier(undefined, false); expect(value).toBe(0); @@ -296,11 +291,6 @@ describe("PricingUtils Tests", () => { }); describe("with multimaster", () => { - it("should return 0 for null", () => { - const value = PricingUtils.getRegionMultiplier(null, true); - expect(value).toBe(0); - }); - it("should return 0 for undefined", () => { const value = PricingUtils.getRegionMultiplier(undefined, true); expect(value).toBe(0); @@ -450,11 +440,6 @@ describe("PricingUtils Tests", () => { }); describe("normalizeNumberOfRegions()", () => { - it("should return 0 for null", () => { - const value = PricingUtils.normalizeNumber(null); - expect(value).toBe(0); - }); - it("should return 0 for undefined", () => { const value = PricingUtils.normalizeNumber(undefined); expect(value).toBe(0); diff --git a/src/Utils/QueryUtils.test.ts b/src/Utils/QueryUtils.test.ts index 80ed18620..72a568a3a 100644 --- a/src/Utils/QueryUtils.test.ts +++ b/src/Utils/QueryUtils.test.ts @@ -5,23 +5,19 @@ import * as ViewModels from "../Contracts/ViewModels"; import * as QueryUtils from "./QueryUtils"; describe("Query Utils", () => { - function generatePartitionKeyForPath(path: string): DataModels.PartitionKey { + const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => { return { paths: [path], kind: "Hash", version: 2, }; - } + }; describe("buildDocumentsQueryPartitionProjections()", () => { it("should return empty string if partition key is undefined", () => { expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", undefined)).toBe(""); }); - it("should return empty string if partition key is null", () => { - expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", null)).toBe(""); - }); - it("should replace slashes and embed projection in square braces", () => { const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a"); const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey); diff --git a/src/Utils/StringUtils.test.ts b/src/Utils/StringUtils.test.ts index f725e9428..bd626f492 100644 --- a/src/Utils/StringUtils.test.ts +++ b/src/Utils/StringUtils.test.ts @@ -3,27 +3,27 @@ import * as StringUtils from "./StringUtils"; describe("StringUtils", () => { describe("stripSpacesFromString()", () => { it("should strip all spaces from input string", () => { - const transformedString: string = StringUtils.stripSpacesFromString("a b c"); + const transformedString: string | undefined = StringUtils.stripSpacesFromString("a b c"); expect(transformedString).toBe("abc"); }); it("should return original string if input string has no spaces", () => { - const transformedString: string = StringUtils.stripSpacesFromString("abc"); + const transformedString: string | undefined = StringUtils.stripSpacesFromString("abc"); expect(transformedString).toBe("abc"); }); it("should return undefined if input is undefined", () => { - const transformedString: string = StringUtils.stripSpacesFromString(undefined); + const transformedString: string | undefined = StringUtils.stripSpacesFromString(undefined); expect(transformedString).toBeUndefined(); }); it("should return undefined if input is undefiend", () => { - const transformedString: string = StringUtils.stripSpacesFromString(undefined); + const transformedString: string | undefined = StringUtils.stripSpacesFromString(undefined); expect(transformedString).toBe(undefined); }); it("should return empty string if input is an empty string", () => { - const transformedString: string = StringUtils.stripSpacesFromString(""); + const transformedString: string | undefined = StringUtils.stripSpacesFromString(""); expect(transformedString).toBe(""); }); }); diff --git a/src/Utils/StringUtils.ts b/src/Utils/StringUtils.ts index 2618544c3..02ceba5f2 100644 --- a/src/Utils/StringUtils.ts +++ b/src/Utils/StringUtils.ts @@ -1,4 +1,4 @@ -export function stripSpacesFromString(inputString: string): string { +export function stripSpacesFromString(inputString?: string): string | undefined { if (inputString === undefined || typeof inputString !== "string") { return inputString; } diff --git a/src/hooks/useDatabaseAccounts.tsx b/src/hooks/useDatabaseAccounts.tsx index 378d4639f..c120d6a71 100644 --- a/src/hooks/useDatabaseAccounts.tsx +++ b/src/hooks/useDatabaseAccounts.tsx @@ -15,7 +15,7 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken: let accounts: Array = []; - let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-06-01-preview`; + let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2021-06-15`; while (nextLink) { const response: Response = await fetch(nextLink, { headers }); diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 3589b479a..86200808c 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -105,7 +105,9 @@ async function configureHostedWithAAD(config: AAD): Promise { aadToken = aadTokenResponse.accessToken; } try { - keys = await listKeys(subscriptionId, resourceGroup, account.name); + if (!account.properties.disableLocalAuth) { + keys = await listKeys(subscriptionId, resourceGroup, account.name); + } } catch (e) { if (userContext.features.enableAadDataPlane) { console.warn(e); @@ -326,8 +328,8 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) { userContext.features.autoscaleDefault; } - if (inputs.flights.indexOf(Flights.SchemaAnalyzer) !== -1) { - userContext.features.enableSchemaAnalyzer = true; + if (inputs.flights.indexOf(Flights.PartitionKeyTest) !== -1) { + userContext.features.partitionKeyDefault = true; } } } diff --git a/src/hooks/useNotebookSnapshotStore.ts b/src/hooks/useNotebookSnapshotStore.ts index a40657d0d..841f6b5d1 100644 --- a/src/hooks/useNotebookSnapshotStore.ts +++ b/src/hooks/useNotebookSnapshotStore.ts @@ -1,8 +1,8 @@ import create, { UseStore } from "zustand"; export interface NotebookSnapshotHooks { - snapshot: string; - error: string; + snapshot?: string; + error?: string; setSnapshot: (imageSrc: string) => void; setError: (error: string) => void; } diff --git a/src/hooks/usePortalAccessToken.tsx b/src/hooks/usePortalAccessToken.tsx index da5cbe2f8..fdccc84a7 100644 --- a/src/hooks/usePortalAccessToken.tsx +++ b/src/hooks/usePortalAccessToken.tsx @@ -24,8 +24,8 @@ export async function fetchAccessData(portalToken: string): Promise(); +export function useTokenMetadata(token: string): AccessInputMetadata | undefined { + const [state, setState] = useState(); useEffect(() => { if (token) { diff --git a/src/hooks/useSidePanel.ts b/src/hooks/useSidePanel.ts index 26fce5954..e1558b910 100644 --- a/src/hooks/useSidePanel.ts +++ b/src/hooks/useSidePanel.ts @@ -2,14 +2,17 @@ import create, { UseStore } from "zustand"; export interface SidePanelState { isOpen: boolean; + panelWidth: string; panelContent?: JSX.Element; headerText?: string; - openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void; + openSidePanel: (headerText: string, panelContent: JSX.Element, panelWidth?: string, onClose?: () => void) => void; closeSidePanel: () => void; } export const useSidePanel: UseStore = create((set) => ({ isOpen: false, - openSidePanel: (headerText, panelContent) => set((state) => ({ ...state, headerText, panelContent, isOpen: true })), + panelWidth: "440px", + openSidePanel: (headerText, panelContent, panelWidth = "440px") => + set((state) => ({ ...state, headerText, panelContent, panelWidth, isOpen: true })), closeSidePanel: () => set((state) => ({ ...state, isOpen: false })), })); diff --git a/src/index.html b/src/index.html index e9672edd7..5332ae673 100644 --- a/src/index.html +++ b/src/index.html @@ -8,54 +8,7 @@ - -
-
- Azure Cosmos DB - - Create an Azure Cosmos DB account - - Azure Cosmos DB Emulator -
-
- - - - - - +
diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 8874bae4a..af68a47dc 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -1,18 +1,17 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Cassandra keyspace and table CRUD", async () => { const keyspaceId = generateUniqueName("keyspace"); const tableId = generateUniqueName("table"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); await explorer.click('[data-test="New Table"]'); await explorer.click('[aria-label="Keyspace id"]'); @@ -20,9 +19,9 @@ test("Cassandra keyspace and table CRUD", async () => { await explorer.click('[aria-label="addCollection-tableId"]'); await explorer.fill('[aria-label="addCollection-tableId"]', tableId); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${keyspaceId}`); - await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); + await explorer.click(`.nodeItem >> text=${keyspaceId}`); + await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index cf52b5bb4..0b2bf5090 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -1,18 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Graph CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and graph await explorer.click('[data-test="New Graph"]'); @@ -20,11 +18,11 @@ test("Graph CRUD", async () => { await explorer.fill('[aria-label="Graph id"]', containerId); await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `.nodeItem >> text=${containerId}`); + await explorer.click(`.nodeItem >> text=${databaseId}`); + await explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and graph - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Graph")'); + await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Graph")'); await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 64ccbba42..53d343882 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,18 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and collection await explorer.click('[data-test="New Collection"]'); @@ -20,10 +18,10 @@ test("Mongo CRUD", async () => { await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Shard key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `.nodeItem >> text=${containerId}`); + await explorer.click(`.nodeItem >> text=${databaseId}`); + await explorer.click(`.nodeItem >> text=${containerId}`); // Create indexing policy - await safeClick(explorer, ".nodeItem >> text=Settings"); + await explorer.click(".nodeItem >> text=Settings"); await explorer.click('button[role="tab"]:has-text("Indexing Policy")'); await explorer.click('[aria-label="Index Field Name 0"]'); await explorer.fill('[aria-label="Index Field Name 0"]', "foo"); @@ -34,8 +32,8 @@ test("Mongo CRUD", async () => { await explorer.click('[aria-label="Delete index Button"]'); await explorer.click('[data-test="Save"]'); // Delete database and collection - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); + await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Collection")'); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index 30fdfd5fe..5b3845a14 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -1,18 +1,16 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Create new database and collection await explorer.click('[data-test="New Collection"]'); @@ -20,11 +18,11 @@ test("Mongo CRUD", async () => { await explorer.fill('[aria-label="Collection id"]', containerId); await explorer.fill('[aria-label="Shard key"]', "pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `.nodeItem >> text=${containerId}`); + explorer.click(`.nodeItem >> text=${databaseId}`); + explorer.click(`.nodeItem >> text=${containerId}`); // Delete database and collection - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); + explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + explorer.click('button[role="menuitem"]:has-text("Delete Collection")'); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); diff --git a/test/notebooks/upload.spec.ts b/test/notebooks/upload.spec.ts index 36b4658bf..5f2483531 100644 --- a/test/notebooks/upload.spec.ts +++ b/test/notebooks/upload.spec.ts @@ -2,6 +2,7 @@ import { jest } from "@jest/globals"; import "expect-playwright"; import fs from "fs"; import path from "path"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); const filename = "GettingStarted.ipynb"; @@ -11,10 +12,7 @@ fs.copyFileSync(path.join(__dirname, filename), path.join(__dirname, fileToUploa test("Notebooks", async () => { await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); // Upload and Delete Notebook await explorer.click('[data-test="My Notebooks"] [aria-label="More"]'); await explorer.click('button[role="menuitem"]:has-text("Upload File")'); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index e68930083..4deb3e30e 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,27 +1,24 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("SQL CRUD", async () => { const databaseId = generateUniqueName("db"); const containerId = generateUniqueName("container"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); - + const explorer = await waitForExplorer(); await explorer.click('[data-test="New Container"]'); await explorer.fill('[aria-label="New database id"]', databaseId); await explorer.fill('[aria-label="Container id"]', containerId); await explorer.fill('[aria-label="Partition key"]', "/pk"); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `.nodeItem >> text=${databaseId}`); - await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")'); + await explorer.click(`.nodeItem >> text=${databaseId}`); + await explorer.click(`[data-test="${containerId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Container")'); await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId); await explorer.click('[aria-label="OK"]'); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index b2337baa9..5dbfc9cfa 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -1,25 +1,24 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { safeClick } from "../utils/safeClick"; import { generateUniqueName } from "../utils/shared"; +import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Tables CRUD", async () => { const tableId = generateUniqueName("table"); + page.setDefaultTimeout(50000); await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); - await page.waitForSelector("iframe"); - const explorer = page.frame({ - name: "explorer", - }); + const explorer = await waitForExplorer(); + await page.waitForSelector('text="Querying databases"', { state: "detached" }); await explorer.click('[data-test="New Table"]'); await explorer.fill('[aria-label="Table id"]', tableId); await explorer.click("#sidePanelOkButton"); - await safeClick(explorer, `[data-test="TablesDB"]`); - await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); - await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); + await explorer.click(`[data-test="TablesDB"]`); + await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`); + await explorer.click('button[role="menuitem"]:has-text("Delete Table")'); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.click('[aria-label="OK"]'); await expect(explorer).not.toHaveText(".dataResourceTree", tableId); diff --git a/test/utils/safeClick.ts b/test/utils/safeClick.ts deleted file mode 100644 index d0c307bd0..000000000 --- a/test/utils/safeClick.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Frame } from "playwright"; - -export async function safeClick(page: Frame, selector: string): Promise { - // TODO: Remove. Playwright does this for you... mostly. - // But our knockout+react setup sometimes leaves dom nodes detached and even playwright can't recover. - // Resource tree is particually bad. - // Ideally this should only be added as a last resort - await page.waitForSelector(selector); - await page.waitForTimeout(5000); - await page.click(selector); -} diff --git a/test/utils/waitForExplorer.ts b/test/utils/waitForExplorer.ts new file mode 100644 index 000000000..30675a0cc --- /dev/null +++ b/test/utils/waitForExplorer.ts @@ -0,0 +1,9 @@ +import { Frame } from "playwright"; + +export const waitForExplorer = async (): Promise => { + await page.waitForSelector("iframe"); + await page.waitForTimeout(5000); + return page.frame({ + name: "explorer", + }); +}; diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 07976aaa1..d48357cda 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -26,7 +26,6 @@ "./src/Common/ObjectCache.ts", "./src/Common/OfferUtility.test.ts", "./src/Common/OfferUtility.ts", - "./src/Common/ResourceTree.tsx", "./src/Common/Splitter.ts", "./src/Common/ThemeUtility.ts", "./src/Common/UrlUtility.ts", @@ -58,6 +57,7 @@ "./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx", "./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx", "./src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx", + "./src/Explorer/Notebook/NotebookRenderer/StatusBar.tsx", "./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx", "./src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx", "./src/Explorer/Notebook/NotebookUtil.ts", @@ -65,12 +65,16 @@ "./src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts", "./src/Explorer/OpenFullScreen.test.tsx", "./src/Explorer/OpenFullScreen.tsx", + "./src/Explorer/Panes/PanelContainerComponent.test.tsx", + "./src/Explorer/Panes/PanelContainerComponent.tsx", "./src/Explorer/Panes/PanelFooterComponent.tsx", "./src/Explorer/Panes/PanelInfoErrorComponent.tsx", "./src/Explorer/Panes/PanelLoadingScreen.tsx", + "./src/Explorer/Panes/PanelStyles.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts", "./src/Explorer/Tables/Constants.ts", + "./src/Explorer/Tables/CqlUtilities.test.ts", "./src/Explorer/Tables/CqlUtilities.ts", "./src/Explorer/Tables/DataTable/CacheBase.ts", "./src/Explorer/Tables/Entities.ts", @@ -79,11 +83,12 @@ "./src/Explorer/Tree/AccessibleVerticalList.ts", "./src/GitHub/GitHubConnector.ts", "./src/HostedExplorerChildFrame.ts", - "./src/Index.ts", "./src/Platform/Hosted/Authorization.ts", "./src/Platform/Hosted/Components/MeControl.test.tsx", "./src/Platform/Hosted/Components/MeControl.tsx", "./src/Platform/Hosted/Components/SignInButton.tsx", + "./src/Platform/Hosted/HostedUtils.test.ts", + "./src/Platform/Hosted/HostedUtils.ts", "./src/Platform/Hosted/extractFeatures.test.ts", "./src/Platform/Hosted/extractFeatures.ts", "./src/ReactDevTools.ts", @@ -93,7 +98,9 @@ "./src/Shared/Constants.ts", "./src/Shared/DefaultExperienceUtility.ts", "./src/Shared/ExplorerSettings.ts", + "./src/Shared/LocalStorageUtility.ts", "./src/Shared/PriceEstimateCalculator.ts", + "./src/Shared/SessionStorageUtility.ts", "./src/Shared/StorageUtility.test.ts", "./src/Shared/StorageUtility.ts", "./src/Shared/StringUtility.test.ts", @@ -105,12 +112,15 @@ "./src/Utils/Base64Utils.test.ts", "./src/Utils/Base64Utils.ts", "./src/Utils/BlobUtils.ts", + "./src/Utils/CapabilityUtils.ts", + "./src/Utils/CloudUtils.ts", "./src/Utils/GitHubUtils.test.ts", "./src/Utils/GitHubUtils.ts", "./src/Utils/MessageValidation.test.ts", "./src/Utils/MessageValidation.ts", "./src/Utils/NotificationConsoleUtils.ts", "./src/Utils/PricingUtils.ts", + "./src/Utils/StringUtils.test.ts", "./src/Utils/StringUtils.ts", "./src/Utils/StyleUtils.ts", "./src/Utils/WindowUtils.test.ts", @@ -119,6 +129,8 @@ "./src/hooks/useDirectories.tsx", "./src/hooks/useFullScreenURLs.tsx", "./src/hooks/useGraphPhoto.tsx", + "./src/hooks/useNotebookSnapshotStore.ts", + "./src/hooks/usePortalAccessToken.tsx", "./src/hooks/useNotificationConsole.ts", "./src/hooks/useObservable.ts", "./src/hooks/useSidePanel.ts", @@ -126,7 +138,9 @@ "./src/quickstart.ts", "./src/setupTests.ts", "./src/userContext.test.ts", - "src/Common/EntityValue.tsx" + "src/Common/EntityValue.tsx", + "./src/Platform/Hosted/Components/SwitchAccount.tsx", + "./src/Platform/Hosted/Components/SwitchSubscription.tsx" ], "include": [ "src/CellOutputViewer/transforms/**/*", diff --git a/webpack.config.js b/webpack.config.js index 12c980224..054b06984 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -198,7 +198,7 @@ module.exports = function (_env = {}, argv = {}) { mode: mode, entry: { main: "./src/Main.tsx", - index: "./src/Index.ts", + index: "./src/Index.tsx", quickstart: "./src/quickstart.ts", hostedExplorer: "./src/HostedExplorer.tsx", testExplorer: "./test/testExplorer/TestExplorer.ts",