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 15d8762b6..7a5d06bbf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -191,4 +191,4 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx src/Explorer/Tree/ResourceTreeAdapter.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/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 fc97d559d..cbdd2da8e 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -95,6 +95,7 @@ export class Flights { public static readonly MongoIndexing = "mongoindexing"; public static readonly AutoscaleTest = "autoscaletest"; public static readonly PartitionKeyTest = "partitionkeytest"; + public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; } export class AfecFeatures { 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..18a769b12 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/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index ad9805348..449627d13 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -23,13 +23,75 @@ export interface DialogState { dialogProps?: DialogProps; openDialog: (props: DialogProps) => void; closeDialog: () => void; + showOkCancelModalDialog: ( + title: string, + subText: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + primaryButtonDisabled?: boolean + ) => void; + showOkModalDialog: (title: string, subText: string) => void; } -export const useDialog: UseStore = create((set) => ({ +export const useDialog: UseStore = create((set, get) => ({ visible: false, openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), closeDialog: () => - set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), + set( + (state) => ({ + visible: false, + openDialog: state.openDialog, + closeDialog: state.closeDialog, + showOkCancelModalDialog: state.showOkCancelModalDialog, + showOkModalDialog: state.showOkModalDialog, + }), + true // TODO: This probably should not be true but its causing a prod bug so easier to just set the proper state above + ), + showOkCancelModalDialog: ( + title: string, + subText: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + primaryButtonDisabled?: boolean + ): void => + get().openDialog({ + isModal: true, + title, + subText, + primaryButtonText: okLabel, + secondaryButtonText: cancelLabel, + onPrimaryButtonClick: () => { + get().closeDialog(); + onOk && onOk(); + }, + onSecondaryButtonClick: () => { + get().closeDialog(); + onCancel && onCancel(); + }, + choiceGroupProps, + textFieldProps, + primaryButtonDisabled, + }), + showOkModalDialog: (title: string, subText: string): void => + get().openDialog({ + isModal: true, + title, + subText, + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: () => { + get().closeDialog(); + }, + onSecondaryButtonClick: undefined, + }), })); export interface TextFieldProps extends ITextFieldProps { diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeahead.less b/src/Explorer/Controls/InputTypeahead/InputTypeahead.less index e56bfaa51..1d68e3b7e 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeahead.less +++ b/src/Explorer/Controls/InputTypeahead/InputTypeahead.less @@ -5,6 +5,9 @@ display: inline-block; width: 100%; + .input-type-head-text-field { + width: 100%; + } textarea { width: 100%; line-height: 1; @@ -21,4 +24,11 @@ } } } - +.input-typeahead-chocies-container { + border: 1px solid lightgrey; + padding: 5px 10px 5px 10px; + cursor: pointer; + .choice-caption{ + font-size: 14px; + } +} \ No newline at end of file diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx index 69f01a745..2e9a2d104 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx +++ b/src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx @@ -6,14 +6,13 @@ * typeaheadOverrideOptions: { dynamic:false } * */ -import "jquery-typeahead"; +import { getTheme, IconButton, IIconProps, List, Stack, TextField } from "@fluentui/react"; import * as React from "react"; -import { KeyCodes } from "../../../Common/Constants"; import "./InputTypeahead.less"; export interface Item { caption: string; - value: any; + value: string; } /** @@ -75,170 +74,125 @@ export interface InputTypeaheadComponentProps { useTextarea?: boolean; } -interface OnClickItem { - matchedKey: string; - value: any; - caption: string; - group: string; +interface InputTypeaheadComponentState { + isSuggestionVisible: boolean; + selectedChoice: Item; + filteredChoices: Item[]; } -interface Cache { - inputValue: string; - selection: Item; -} - -interface InputTypeaheadComponentState {} - export class InputTypeaheadComponent extends React.Component< InputTypeaheadComponentProps, InputTypeaheadComponentState > { - private inputElt: HTMLElement; - private containerElt: HTMLElement; - - private cache: Cache; - private inputValue: string; - private selection: Item; - - public constructor(props: InputTypeaheadComponentProps) { + constructor(props: InputTypeaheadComponentProps) { super(props); - this.cache = { - inputValue: null, - selection: null, + this.state = { + isSuggestionVisible: false, + filteredChoices: [], + selectedChoice: { + caption: "", + value: "", + }, }; } - /** - * Props have changed - * @param prevProps - * @param prevState - * @param snapshot - */ - public componentDidUpdate( - prevProps: InputTypeaheadComponentProps, - prevState: InputTypeaheadComponentState, - snapshot: any - ): void { - if (prevProps.defaultValue !== this.props.defaultValue) { - $(this.inputElt).val(this.props.defaultValue); - this.initializeTypeahead(); - } - } - - /** - * Executed once react is done building the DOM for this component - */ - public componentDidMount(): void { - this.initializeTypeahead(); - } - - public render(): JSX.Element { + private onRenderCell = (item: Item): JSX.Element => { return ( - -
) => this.onKeyDown(event)} - > -
(this.containerElt = input)}> -
- - {this.props.useTextarea ? ( -