From 6925fa8e4e49766720137a83e3c6bff52e363b1b Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:55:08 -0700 Subject: [PATCH 01/67] Replace Entra app client secret auth with OpenID Connect in E2E tests. (#1792) * Use Az login with OpenID connection to get test credentials. * Set subscription id environment variable. * Update testExplorer and cleanup job. * Retrieve access token in test case and pass to testExplorer. * Add debug tracing for tests. * Set up other mongo test to use Az CLI creds. * Revert subscription id retrieval. * Add CLI credentials retrieval to rest of tests. * Fix missing imports. * Clean up redundant code. * Remove commented import statement. --- .github/workflows/ci.yml | 14 ++++++++++++-- .github/workflows/cleanup.yml | 15 +++++++++++++-- test/cassandra/container.spec.ts | 7 +++++-- test/graph/container.spec.ts | 7 +++++-- test/mongo/container.spec.ts | 7 +++++-- test/mongo/container32.spec.ts | 7 +++++-- test/selfServe/selfServeExample.spec.ts | 7 ++++++- test/sql/container.spec.ts | 7 +++++-- test/sql/resourceToken.spec.ts | 10 +++------- test/tables/container.spec.ts | 6 ++++-- test/testExplorer/TestExplorer.ts | 19 +------------------ test/utils/shared.ts | 11 +++++++++++ utils/cleanupDBs.js | 9 +++------ 13 files changed, 78 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a53b92645..fd097a194 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: pull_request: branches: - master +permissions: + id-token: write + contents: read jobs: codemetrics: runs-on: ubuntu-latest @@ -134,7 +137,7 @@ jobs: runs-on: ubuntu-latest env: NODE_TLS_REJECT_UNAUTHORIZED: 0 - NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} strategy: fail-fast: false matrix: @@ -145,11 +148,18 @@ jobs: - ./test/mongo/container.spec.ts - ./test/mongo/container32.spec.ts - ./test/selfServe/selfServeExample.spec.ts - # - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off - ./test/sql/resourceToken.spec.ts - ./test/tables/container.spec.ts steps: - uses: actions/checkout@v4 + + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x uses: actions/setup-node@v4 with: diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 229477f0b..6698951ae 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -9,6 +9,10 @@ on: # Once every hour - cron: "0 15 * * *" +permissions: + id-token: write + contents: read + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" @@ -16,10 +20,17 @@ jobs: name: "Cleanup Test Database Accounts" runs-on: ubuntu-latest env: - NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }} - NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} steps: - uses: actions/checkout@v2 + + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x uses: actions/setup-node@v1 with: diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 48acddb79..80d5df41d 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } 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"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`); await page.waitForSelector("iframe"); const explorer = await waitForExplorer(); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index 3e6155f1c..e7f288da5 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Graph CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and graph diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 88a311784..baafefbbe 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and collection diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index 25466e266..c71f9d0cc 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and collection diff --git a/test/selfServe/selfServeExample.spec.ts b/test/selfServe/selfServeExample.spec.ts index 7e10c1ce2..3678f5b35 100644 --- a/test/selfServe/selfServeExample.spec.ts +++ b/test/selfServe/selfServeExample.spec.ts @@ -1,5 +1,10 @@ +import { getAzureCLICredentialsToken } from "../utils/shared"; + test("Self Serve", async () => { - await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html"); + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); + + await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`); const handle = await page.waitForSelector("iframe"); const frame = await handle.contentFrame(); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index aead57b92..a1aacfd42 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("SQL CRUD", async () => { const databaseId = generateUniqueName("db"); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`); const explorer = await waitForExplorer(); await explorer.click('[data-test="New Container"]'); diff --git a/test/sql/resourceToken.spec.ts b/test/sql/resourceToken.spec.ts index f2e9d94ef..18228c7ed 100644 --- a/test/sql/resourceToken.spec.ts +++ b/test/sql/resourceToken.spec.ts @@ -1,19 +1,15 @@ import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosClient, PermissionMode } from "@azure/cosmos"; -import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentials } from "../utils/shared"; jest.setTimeout(120000); -const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4"; -const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; -const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; +const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? ""; const resourceGroupName = "runners"; test("Resource token", async () => { - const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); + const credentials = await getAzureCLICredentials(); const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us"); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us"); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index d8daa0a16..98687d60f 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -1,15 +1,17 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Tables CRUD", async () => { const tableId = generateUniqueName("table"); + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-tables-runner&token=${token}`); const explorer = await waitForExplorer(); await page.waitForSelector('text="Querying databases"', { state: "detached" }); diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index 684673590..a415b5bd6 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import { ClientSecretCredential } from "@azure/identity"; import "../../less/hostedexplorer.less"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels"; import { updateUserContext } from "../../src/UserContext"; @@ -11,29 +10,13 @@ const urlSearchParams = new URLSearchParams(window.location.search); const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us"; const selfServeType = urlSearchParams.get("selfServeType") || "example"; const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache"; - -if (!process.env.AZURE_CLIENT_SECRET) { - throw new Error( - "process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server", - ); -} - -// Azure SDK clients accept the credential as a parameter -const credentials = new ClientSecretCredential( - process.env.AZURE_TENANT_ID, - process.env.AZURE_CLIENT_ID, - process.env.AZURE_CLIENT_SECRET, - { - authorityHost: "https://localhost:1234", - }, -); +const token = urlSearchParams.get("token"); console.log("Resource Group:", resourceGroup); console.log("Subcription: ", subscriptionId); console.log("Account Name: ", accountName); const initTestExplorer = async (): Promise => { - const { token } = await credentials.getToken("https://management.azure.com//.default"); updateUserContext({ authorizationToken: `bearer ${token}`, }); diff --git a/test/utils/shared.ts b/test/utils/shared.ts index 118736129..59ef0994c 100644 --- a/test/utils/shared.ts +++ b/test/utils/shared.ts @@ -1,3 +1,4 @@ +import { AzureCliCredentials } from "@azure/ms-rest-nodeauth"; import crypto from "crypto"; export function generateUniqueName(baseName = "", length = 4): string { @@ -7,3 +8,13 @@ export function generateUniqueName(baseName = "", length = 4): string { export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string { return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; } + +export async function getAzureCLICredentials(): Promise { + return await AzureCliCredentials.create(); +} + +export async function getAzureCLICredentialsToken(): Promise { + const credentials = await getAzureCLICredentials(); + const token = (await credentials.getToken()).accessToken; + return token; +} diff --git a/utils/cleanupDBs.js b/utils/cleanupDBs.js index 72fcfbafd..b2bbf0be8 100644 --- a/utils/cleanupDBs.js +++ b/utils/cleanupDBs.js @@ -2,10 +2,7 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const ms = require("ms"); -const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"]; -const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; -const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; +const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"]; const resourceGroupName = "runners"; const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); @@ -19,7 +16,7 @@ function friendlyTime(date) { } async function main() { - const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); + const credentials = await msRestNodeAuth.AzureCliCredentials.create(); const client = new CosmosDBManagementClient(credentials, subscriptionId); const accounts = await client.databaseAccounts.list(resourceGroupName); for (const account of accounts) { @@ -38,7 +35,7 @@ async function main() { } else if (account.capabilities.find((c) => c.name === "EnableCassandra")) { const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces( resourceGroupName, - account.name + account.name, ); for (const database of cassandraDatabases) { const timestamp = Number(database.resource._ts) * 1000; From dfcb7719392d77852e0987141aba703cdec90b65 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 9 Apr 2024 16:45:36 -0400 Subject: [PATCH 02/67] Activate Mongo Proxy and Cassandra Proxy in Prod (#1794) * activate Mongo Proxy and Cassandra Proxy in Prod * fix bug that blocked local mongo proxy and cassandra proxy development * fix pr check tests --------- Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.ts | 11 +++++++++-- src/Explorer/Tables/TableDataClient.ts | 13 ++++++++++--- src/Explorer/Tabs/Tabs.tsx | 7 ++++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 749248919..44400d874 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -690,9 +690,16 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter } function useMongoProxyEndpoint(api: string): boolean { - const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac]; + const activeMongoProxyEndpoints: string[] = [ + MongoProxyEndpoints.Development, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + ]; let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; - if (userContext.databaseAccount.properties.ipRules?.length > 0) { + if ( + configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development && + userContext.databaseAccount.properties.ipRules?.length > 0 + ) { canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; } diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 097ade395..c6882bc33 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -3,6 +3,7 @@ import * as ko from "knockout"; import Q from "q"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; +import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants"; import { handleError } from "../../Common/ErrorHandlingUtils"; import * as HeadersUtility from "../../Common/HeadersUtility"; import { createDocument } from "../../Common/dataAccess/createDocument"; @@ -19,7 +20,6 @@ import Explorer from "../Explorer"; import * as TableConstants from "./Constants"; import * as Entities from "./Entities"; import * as TableEntityProcessor from "./TableEntityProcessor"; -import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants"; export interface CassandraTableKeys { partitionKeys: CassandraTableKey[]; @@ -732,9 +732,16 @@ export class CassandraAPIDataClient extends TableDataClient { } private useCassandraProxyEndpoint(api: string): boolean { - const activeCassandraProxyEndpoints: string[] = [CassandraProxyEndpoints.Development, CassandraProxyEndpoints.Mpac]; + const activeCassandraProxyEndpoints: string[] = [ + CassandraProxyEndpoints.Development, + CassandraProxyEndpoints.Mpac, + CassandraProxyEndpoints.Prod, + ]; let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; - if (userContext.databaseAccount.properties.ipRules?.length > 0) { + if ( + configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development && + userContext.databaseAccount.properties.ipRules?.length > 0 + ) { canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; } diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 4af95118b..53678ed5a 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -324,7 +324,12 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; - if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) { + if ( + ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) || + (userContext.apiType === "Cassandra" && + configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) && + ipRules?.length + ) { const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT]; const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange); const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) => From 953bef404ba177e3030e811bf5c4c97e7bcda08c Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:43:46 -0700 Subject: [PATCH 03/67] Set backend endpoints in testExplorer to use MPAC. (#1797) --- test/testExplorer/TestExplorer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index a415b5bd6..4dbeb86b6 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -35,6 +35,9 @@ const initTestExplorer = async (): Promise => { dnsSuffix: "documents.azure.com", serverId: "prod1", extensionEndpoint: "/proxy", + portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com", + mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com", + cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com", subscriptionType: 3, quotaId: "Internal_2014-09-01", isTryCosmosDBSubscription: false, From 00a816c4881a897a93959a124ab8a54e8fea6758 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Sat, 13 Apr 2024 15:19:56 -0500 Subject: [PATCH 04/67] set the value in the editor for results (#1799) --- src/Explorer/Controls/Editor/EditorReact.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 1d7a7381b..47268c856 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -54,13 +54,17 @@ export class EditorReact extends React.Component Date: Mon, 15 Apr 2024 15:47:58 -0400 Subject: [PATCH 05/67] Cassandra API create table error messages swallowed by the Portal and shown as "undefined". (#1790) * changed error message variable * changed other error messages * Added check in case responseJSON is missing * created error const --- src/Explorer/Tables/TableDataClient.ts | 39 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index c6882bc33..a9d7c96a6 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -172,8 +172,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(entity); }, (error) => { - handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -406,12 +407,13 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(); }, (error) => { + const errorText = error.responseJSON?.message ?? JSON.stringify(error); handleError( - error, + errorText, "CreateKeyspaceCassandra", `Error while creating a keyspace with query ${createKeyspaceQuery}`, ); - deferred.reject(error); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -444,8 +446,13 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(); }, (error) => { - handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError( + errorText, + "CreateTableCassandra", + `Error while creating a table with query ${createTableQuery}`, + ); + deferred.reject(errorText); }, ) .finally(clearInProgressMessage); @@ -493,8 +500,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data); }, (error: any) => { - handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); @@ -533,8 +541,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data); }, (error: any) => { - handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); @@ -578,8 +587,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data.columns); }, (error: any) => { - handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); @@ -618,8 +628,9 @@ export class CassandraAPIDataClient extends TableDataClient { deferred.resolve(data.columns); }, (error: any) => { - handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); - deferred.reject(error); + const errorText = error.responseJSON?.message ?? JSON.stringify(error); + handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(errorText); }, ) .done(clearInProgressMessage); From 6c9673975ad37df7343f889cfa21a2a2d0c83c85 Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:24:14 -0400 Subject: [PATCH 06/67] Added hyphen to prohibited characters in keyspace name title (#1800) --- .../CassandraAddCollectionPane.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index dbef20b12..cd91a6628 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -202,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent Date: Tue, 16 Apr 2024 18:47:43 -0400 Subject: [PATCH 07/67] Is executing is false (#1801) --- src/Explorer/Panes/Tables/AddTableEntityPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx index 1f23a60bc..9ad999aa0 100644 --- a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx @@ -124,8 +124,8 @@ export const AddTableEntityPanel: FunctionComponent = setIsExecuting(true); const entity: Entities.ITableEntity = entityFromAttributes(entities); - const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); try { + const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity); await tableEntityListViewModel.addEntityToCache(newEntity); if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) { tableEntityListViewModel.redrawTableThrottled(); From a44ed1f45c0ae6c977333be0cf96d53e1db29548 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 17 Apr 2024 11:19:09 -0700 Subject: [PATCH 08/67] [Task 3061766] Global Keyboard Shortcuts, implemented through the Command Bar (#1789) * keyboard shortcuts using tinykeys * refmt and fix lints * retarget keyboard shortcuts to the body instead of the root element of the React component tree * refmt * Update src/Explorer/Menus/CommandBar/CommandBarUtil.tsx Co-authored-by: Laurent Nguyen * add Save binding to New Item command bar --------- Co-authored-by: Laurent Nguyen --- package-lock.json | 6 ++ package.json | 1 + .../CommandButton/CommandButtonComponent.tsx | 10 ++- .../CommandBar/CommandBarComponentAdapter.tsx | 6 ++ .../CommandBarComponentButtonFactory.tsx | 5 ++ .../Menus/CommandBar/CommandBarUtil.tsx | 26 ++++++ src/Explorer/Tabs/DocumentsTab.ts | 3 + .../Tabs/QueryTab/QueryTabComponent.tsx | 4 + .../StoredProcedureTabComponent.tsx | 4 + src/Explorer/Tabs/TriggerTabContent.tsx | 3 + .../Tabs/UserDefinedFunctionTabContent.tsx | 5 +- src/KeyboardShortcuts.tsx | 89 +++++++++++++++++++ src/Main.tsx | 89 ++++++++++--------- 13 files changed, 206 insertions(+), 45 deletions(-) create mode 100644 src/KeyboardShortcuts.tsx diff --git a/package-lock.json b/package-lock.json index d3a36e3f3..edb334b7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,6 +106,7 @@ "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", + "tinykeys": "2.1.0", "underscore": "1.9.1", "utility-types": "3.10.0", "zustand": "3.5.0" @@ -37786,6 +37787,11 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" }, + "node_modules/tinykeys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinykeys/-/tinykeys-2.1.0.tgz", + "integrity": "sha512-/MESnqBD1xItZJn5oGQ4OsNORQgJfPP96XSGoyu4eLpwpL0ifO0SYR5OD76u0YMhMXsqkb0UqvI9+yXTh4xv8Q==" + }, "node_modules/tinyqueue": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", diff --git a/package.json b/package.json index a7bfec4b8..cc195cd5f 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", + "tinykeys": "2.1.0", "underscore": "1.9.1", "utility-types": "3.10.0", "zustand": "3.5.0" diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index 1e5cfc171..6337f947e 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -1,6 +1,7 @@ /** * React component for Command button component. */ +import { KeyboardAction } from "KeyboardShortcuts"; import * as React from "react"; import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import { KeyCodes } from "../../../Common/Constants"; @@ -30,7 +31,7 @@ export interface CommandButtonComponentProps { /** * Click handler for command button click */ - onCommandClick: (e: React.SyntheticEvent) => void; + onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void; /** * Label for the button @@ -107,10 +108,17 @@ export interface CommandButtonComponentProps { * Vertical bar to divide buttons */ isDivider?: boolean; + /** * Aria-label for the button */ ariaLabel: string; + + /** + * If specified, a keyboard action that should trigger this button's onCommandClick handler when activated. + * If not specified, the button will not be triggerable by keyboard shortcuts. + */ + keyboardAction?: KeyboardAction; } export class CommandButtonComponent extends React.Component { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 141bda577..eaa56591d 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -5,6 +5,7 @@ */ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { useNotebook } from "Explorer/Notebook/useNotebook"; +import { useKeyboardActionHandlers } from "KeyboardShortcuts"; import { userContext } from "UserContext"; import * as React from "react"; import create, { UseStore } from "zustand"; @@ -40,6 +41,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const buttons = useCommandBar((state) => state.contextButtons); const isHidden = useCommandBar((state) => state.isHidden); const backgroundColor = StyleConstants.BaseLight; + const setKeyboardShortcutHandlers = useKeyboardActionHandlers((state) => state.setHandlers); if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { const buttons = @@ -105,6 +107,10 @@ export const CommandBar: React.FC = ({ container }: Props) => { }, }; + const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); + const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); + setKeyboardShortcutHandlers(keyboardHandlers); + return (
{ const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); @@ -312,6 +314,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB id: "newQueryBtn", iconSrc: AddSqlQueryIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_QUERY, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection); @@ -397,6 +400,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps return { iconSrc: BrowseQueriesIcon, iconAlt: label, + keyboardAction: KeyboardAction.OPEN_QUERY, onCommandClick: () => useSidePanel.getState().openSidePanel("Open Saved Queries", ), commandButtonLabel: label, @@ -411,6 +415,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps { return { iconSrc: OpenQueryFromDiskIcon, iconAlt: label, + keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK, onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", ), commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 384bd06cd..fc67ad894 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -7,6 +7,7 @@ import { IDropdownStyles, } from "@fluentui/react"; import { useQueryCopilot } from "hooks/useQueryCopilot"; +import { KeyboardHandlerMap } from "KeyboardShortcuts"; import * as React from "react"; import _ from "underscore"; import ChevronDownIcon from "../../../../images/Chevron_down.svg"; @@ -233,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType, onRender: () => , }; }; + +export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap { + const handlers: KeyboardHandlerMap = {}; + + function createHandlers(buttons: CommandButtonComponentProps[]) { + buttons.forEach((button) => { + if (!button.disabled && button.keyboardAction) { + handlers[button.keyboardAction] = (e) => { + button.onCommandClick(e); + + // If the handler is bound, it means the button is visible and enabled, so we should prevent the default action + return true; + }; + } + + if (button.children && button.children.length > 0) { + createHandlers(button.children); + } + }); + } + + createHandlers(allButtons); + + return handlers; +} diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 3591378da..263fa77bd 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -1,6 +1,7 @@ import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { Platform, configContext } from "ConfigContext"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; +import { KeyboardAction } from "KeyboardShortcuts"; import { QueryConstants } from "Shared/Constants"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import * as ko from "knockout"; @@ -907,6 +908,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -936,6 +938,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveExistingDocumentClick, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index fa849c212..e3f740a25 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { useSelectedNode } from "Explorer/useSelectedNode"; +import { KeyboardAction } from "KeyboardShortcuts"; import { QueryConstants } from "Shared/Constants"; import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; import { Action } from "Shared/Telemetry/TelemetryConstants"; @@ -393,6 +394,7 @@ export default class QueryTabComponent extends React.Component OnExecuteQueryClick(this.props.copilotStore) : this.onExecuteQueryClick, @@ -408,6 +410,7 @@ export default class QueryTabComponent extends React.Component this.queryAbortController.abort(), commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index 98e398e1b..dc179b002 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -1,5 +1,6 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { Pivot, PivotItem } from "@fluentui/react"; +import { KeyboardAction } from "KeyboardShortcuts"; import React from "react"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import DiscardIcon from "../../../../images/discard.svg"; @@ -321,6 +322,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onSaveClick, commandButtonLabel: label, ariaLabel: label, @@ -334,6 +336,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: SaveIcon, iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, onCommandClick: this.onUpdateClick, commandButtonLabel: label, ariaLabel: label, @@ -360,6 +363,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: ExecuteQueryIcon, iconAlt: label, + keyboardAction: KeyboardAction.EXECUTE_ITEM, onCommandClick: () => { this.collection.container.openExecuteSprocParamsPanel(this.node); }, diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index bf756598e..2c7d32ab4 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -1,5 +1,6 @@ import { TriggerDefinition } from "@azure/cosmos"; import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react"; +import { KeyboardAction } from "KeyboardShortcuts"; import React, { Component } from "react"; import DiscardIcon from "../../../images/discard.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; @@ -227,6 +228,7 @@ export class TriggerTabContent extends Component boolean | void; + +export type KeyboardHandlerMap = Partial>; + +/** + * The possible actions that can be triggered by keyboard shortcuts. + */ +export enum KeyboardAction { + NEW_QUERY = "NEW_QUERY", + EXECUTE_ITEM = "EXECUTE_ITEM", + CANCEL_QUERY = "CANCEL_QUERY", + SAVE_ITEM = "SAVE_ITEM", + OPEN_QUERY = "OPEN_QUERY", + OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK", +} + +/** + * The keyboard shortcuts for the application. + * This record maps each action to the keyboard shortcuts that trigger the action. + * Even if an action is specified here, it will not be triggered unless a handler is set for it. + */ +const bindings: Record = { + // NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS. + // See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts. + + [KeyboardAction.NEW_QUERY]: ["$mod+J"], + [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"], + [KeyboardAction.CANCEL_QUERY]: ["Escape"], + [KeyboardAction.SAVE_ITEM]: ["$mod+S"], + [KeyboardAction.OPEN_QUERY]: ["$mod+O"], + [KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"], +}; + +interface KeyboardShortcutState { + /** + * A set of all the keyboard shortcuts handlers. + */ + allHandlers: KeyboardHandlerMap; + + /** + * Sets the keyboard shortcut handlers. + */ + setHandlers: (handlers: KeyboardHandlerMap) => void; +} + +export const useKeyboardActionHandlers: UseStore = create((set) => ({ + allHandlers: {}, + setHandlers: (handlers: Partial>) => { + set({ allHandlers: handlers }); + }, +})); + +function createHandler(action: KeyboardAction): KeyboardActionHandler { + return (e) => { + const state = useKeyboardActionHandlers.getState(); + const handler = state.allHandlers[action]; + if (handler && handler(e)) { + e.preventDefault(); + e.stopPropagation(); + } + }; +} + +const allHandlers: KeyBindingMap = {}; +(Object.keys(bindings) as KeyboardAction[]).forEach((action) => { + const shortcuts = bindings[action]; + shortcuts.forEach((shortcut) => { + allHandlers[shortcut] = createHandler(action); + }); +}); + +export function KeyboardShortcutRoot({ children }: PropsWithChildren) { + useEffect(() => { + // We bind to the body because Fluent UI components sometimes shift focus to the body, which is above the root React component. + tinykeys(document.body, allHandlers); + }, []); + + return <>{children}; +} diff --git a/src/Main.tsx b/src/Main.tsx index c6b79b139..f79ee29f1 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -21,6 +21,7 @@ import "../externals/jquery.typeahead.min.js"; // Image Dependencies import { Platform } from "ConfigContext"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; +import { KeyboardShortcutRoot } from "KeyboardShortcuts"; import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import "../images/favicon.ico"; @@ -91,52 +92,54 @@ const App: React.FunctionComponent = () => { } return ( -
-
-
- {/* Main Command Bar - Start */} - - {/* Collections Tree and Tabs - Begin */} -
- {/* Collections Tree - Start */} - {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && ( -
-
- {/* Collections Tree Expanded - Start */} - - {/* Collections Tree Expanded - End */} - {/* Collections Tree Collapsed - Start */} - - {/* Collections Tree Collapsed - End */} + +
+
+
+ {/* Main Command Bar - Start */} + + {/* Collections Tree and Tabs - Begin */} +
+ {/* Collections Tree - Start */} + {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && ( +
+
+ {/* Collections Tree Expanded - Start */} + + {/* Collections Tree Expanded - End */} + {/* Collections Tree Collapsed - Start */} + + {/* Collections Tree Collapsed - End */} +
-
- )} - -
- {/* Collections Tree and Tabs - End */} - + {/* Collections Tree and Tabs - End */} +
+ + + {} + {} + {} + {}
- - - {} - {} - {} - {} -
+ ); }; From af664326ea499a0a69094398874e2da6973fe7bf Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 17 Apr 2024 15:57:29 -0700 Subject: [PATCH 09/67] Fix issues with the command bar when switching through React and Trigger tabs (#1804) * fix bug in trigger tab that takes over the command bar while open * clear context buttons when a react tab is active * restore unintentionally removed code * reformat --- src/Explorer/Tabs/Tabs.tsx | 4 ++++ src/Explorer/Tabs/TriggerTabContent.tsx | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 53678ed5a..d3732ac1c 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -6,6 +6,7 @@ import { IpRule } from "Contracts/DataModels"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { CollectionTabKind } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; +import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { ConnectTab } from "Explorer/Tabs/ConnectTab"; @@ -297,6 +298,9 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => { }; const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { + // React tabs have no context buttons. + useCommandBar.getState().setContextButtons([]); + // eslint-disable-next-line no-console switch (activeReactTab) { case ReactTabKind.Connect: diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index 2c7d32ab4..23ab9b2e1 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -219,6 +219,18 @@ export class TriggerTabContent extends Component From 98000a27f090ffc73b879b13e0cda75cd5358d0e Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Wed, 17 Apr 2024 19:01:12 -0400 Subject: [PATCH 10/67] Legacy Mongo Shell Mongo Proxy support (#1802) * LMS Mongo Proxy support * change stirng to url for get mongo shell url * fix tests * enable feature flag * fixed unit test --------- Co-authored-by: Asier Isayas --- src/Common/Constants.ts | 2 +- src/Common/MongoProxyClient.ts | 42 ++--- src/ConfigContext.ts | 2 + .../MongoShellTab/MongoShellTabComponent.tsx | 17 +- .../MongoShellTab/getMongoShellOrigin.test.ts | 86 --------- .../Tabs/MongoShellTab/getMongoShellOrigin.ts | 10 - .../MongoShellTab/getMongoShellUrl.test.ts | 174 +----------------- .../Tabs/MongoShellTab/getMongoShellUrl.ts | 40 +--- src/Explorer/Tabs/Tabs.tsx | 2 +- src/Platform/Hosted/extractFeatures.ts | 12 +- src/Utils/EndpointUtils.ts | 2 +- web.config | 2 +- 12 files changed, 52 insertions(+), 339 deletions(-) delete mode 100644 src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts delete mode 100644 src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 1eed03a4f..69bd5ed49 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -138,7 +138,7 @@ export class PortalBackendEndpoints { } export class MongoProxyEndpoints { - public static readonly Development: string = "https://localhost:7238"; + public static readonly Local: string = "https://localhost:7238"; public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 44400d874..907b0305e 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -672,6 +672,27 @@ export function getEndpoint(endpoint: string): string { return url; } +export function useMongoProxyEndpoint(api: string): boolean { + const activeMongoProxyEndpoints: string[] = [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + ]; + let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; + if ( + configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local && + userContext.databaseAccount.properties.ipRules?.length > 0 + ) { + canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; + } + + return ( + canAccessMongoProxy && + configContext.NEW_MONGO_APIS?.includes(api) && + activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT) + ); +} + // TODO: This function throws most of the time except on Forbidden which is a bit strange // It causes problems for TypeScript understanding the types async function errorHandling(response: Response, action: string, params: unknown): Promise { @@ -688,24 +709,3 @@ async function errorHandling(response: Response, action: string, params: unknown export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; } - -function useMongoProxyEndpoint(api: string): boolean { - const activeMongoProxyEndpoints: string[] = [ - MongoProxyEndpoints.Development, - MongoProxyEndpoints.Mpac, - MongoProxyEndpoints.Prod, - ]; - let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; - if ( - configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development && - userContext.databaseAccount.properties.ipRules?.length > 0 - ) { - canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED; - } - - return ( - canAccessMongoProxy && - configContext.NEW_MONGO_APIS?.includes(api) && - activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT) - ); -} diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 435166b90..061f25286 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -83,6 +83,7 @@ let configContext: Readonly = { `^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`, `^https:\\/\\/.*\\.azure-test\\.net$`, + `^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`, ], // Webpack injects this at build time gitSha: process.env.GIT_SHA, hostedExplorerURL: "https://cosmos.azure.com/", @@ -108,6 +109,7 @@ let configContext: Readonly = { "updateDocument", "deleteDocument", "createCollectionWithProxy", + "legacyMongoShell", ], MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index cb47a070d..a89ca5fc7 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -9,7 +9,6 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "../../../Utils/Messa import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; import TabsBase from "../TabsBase"; -import { getMongoShellOrigin } from "./getMongoShellOrigin"; import { getMongoShellUrl } from "./getMongoShellUrl"; //eslint-disable-next-line @@ -35,7 +34,7 @@ export interface IMongoShellTabAccessor { } export interface IMongoShellTabComponentStates { - url: string; + url: URL; } export interface IMongoShellTabComponentProps { @@ -50,13 +49,16 @@ export default class MongoShellTabComponent extends Component< IMongoShellTabComponentStates > { private _logTraces: Map; + private _useMongoProxyEndpoint: boolean; constructor(props: IMongoShellTabComponentProps) { super(props); this._logTraces = new Map(); + this._useMongoProxyEndpoint = userContext.features.enableLegacyMongoShell; + // this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell"); this.state = { - url: getMongoShellUrl(), + url: getMongoShellUrl(this._useMongoProxyEndpoint), }; props.onMongoShellTabAccessor({ @@ -119,9 +121,10 @@ export default class MongoShellTabComponent extends Component< ) + Constants.MongoDBAccounts.defaultPort.toString(); const databaseId = this.props.collection.databaseId; const collectionId = this.props.collection.id(); - const apiEndpoint = configContext.BACKEND_ENDPOINT; + const apiEndpoint = this._useMongoProxyEndpoint + ? configContext.MONGO_PROXY_ENDPOINT + : configContext.BACKEND_ENDPOINT; const encryptedAuthToken: string = userContext.accessToken; - const targetOrigin = getMongoShellOrigin(); shellIframe.contentWindow.postMessage( { @@ -137,7 +140,7 @@ export default class MongoShellTabComponent extends Component< apiEndpoint: apiEndpoint, }, }, - targetOrigin, + window.origin, ); } @@ -218,7 +221,7 @@ export default class MongoShellTabComponent extends Component< name="explorer" className="iframe" style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }} - src={this.state.url} + src={this.state.url.toString()} id={this.props.tabsBaseInstance.tabId} onLoad={(event) => this.setContentFocus(event)} title="Mongo Shell" diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts deleted file mode 100644 index 8f62b2a0c..000000000 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { extractFeatures } from "Platform/Hosted/extractFeatures"; -import { configContext } from "../../../ConfigContext"; -import { updateUserContext } from "../../../UserContext"; -import { getMongoShellOrigin } from "./getMongoShellOrigin"; - -describe("getMongoShellOrigin", () => { - (window as { origin: string }).origin = "window_origin"; - - beforeEach(() => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "false", - "feature.enableLegacyMongoShellV2": "false", - "feature.enableLegacyMongoShellV1Debug": "false", - "feature.enableLegacyMongoShellV2Debug": "false", - "feature.loadLegacyMongoShellFromBE": "false", - }), - ), - }); - }); - - it("should return by default", () => { - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV1", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV2===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV1Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1Debug": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return window.origin when enableLegacyMongoShellV2Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2Debug": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(window.origin); - }); - - it("should return BACKEND_ENDPOINT when loadLegacyMongoShellFromBE===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.loadLegacyMongoShellFromBE": "true", - }), - ), - }); - - expect(getMongoShellOrigin()).toBe(configContext.BACKEND_ENDPOINT); - }); -}); diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts deleted file mode 100644 index 774a4443c..000000000 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellOrigin.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { configContext } from "../../../ConfigContext"; -import { userContext } from "../../../UserContext"; - -export function getMongoShellOrigin(): string { - if (userContext.features.loadLegacyMongoShellFromBE === true) { - return configContext.BACKEND_ENDPOINT; - } - - return window.origin; -} diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts index 1d75f682d..0c138ff61 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts @@ -1,9 +1,9 @@ -import { extractFeatures } from "Platform/Hosted/extractFeatures"; -import { Platform, configContext, resetConfigContext, updateConfigContext } from "../../../ConfigContext"; +import { Platform, resetConfigContext, updateConfigContext } from "../../../ConfigContext"; import { updateUserContext, userContext } from "../../../UserContext"; -import { getExtensionEndpoint, getMongoShellUrl } from "./getMongoShellUrl"; +import { getMongoShellUrl } from "./getMongoShellUrl"; const mongoBackendEndpoint = "https://localhost:1234"; +const hostedExplorerURL = "https://cosmos.azure.com/"; describe("getMongoShellUrl", () => { let queryString = ""; @@ -13,6 +13,7 @@ describe("getMongoShellUrl", () => { updateConfigContext({ BACKEND_ENDPOINT: mongoBackendEndpoint, + hostedExplorerURL: hostedExplorerURL, platform: Platform.Hosted, }); @@ -32,175 +33,18 @@ describe("getMongoShellUrl", () => { cassandraEndpoint: "fakeCassandraEndpoint", }, }, - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "false", - "feature.enableLegacyMongoShellV2": "false", - "feature.enableLegacyMongoShellV1Debug": "false", - "feature.enableLegacyMongoShellV2Debug": "false", - "feature.loadLegacyMongoShellFromBE": "false", - }), - ), portalEnv: "prod", }); queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`; }); - it("should return /mongoshell/indexv2.html by default", () => { - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); + it("should return /indexv2.html by default", () => { + expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`); }); - it("should return /mongoshell/indexv2.html when portalEnv==localhost", () => { - updateUserContext({ - portalEnv: "localhost", - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV1===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/index.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV2===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/indexv2.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV1Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV1Debug": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/debug/index.html?${queryString}`); - }); - - it("should return /mongoshell/index.html when enableLegacyMongoShellV2Debug===true", () => { - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.enableLegacyMongoShellV2Debug": "true", - }), - ), - }); - - expect(getMongoShellUrl()).toBe(`/mongoshell/debug/indexv2.html?${queryString}`); - }); - - describe("loadLegacyMongoShellFromBE===true", () => { - beforeEach(() => { - resetConfigContext(); - updateConfigContext({ - BACKEND_ENDPOINT: mongoBackendEndpoint, - platform: Platform.Hosted, - }); - - updateUserContext({ - features: extractFeatures( - new URLSearchParams({ - "feature.loadLegacyMongoShellFromBE": "true", - }), - ), - }); - }); - - it("should return /mongoshell/index.html", () => { - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - updateConfigContext({ - platform: Platform.Portal, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT !== '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Portal, - BACKEND_ENDPOINT: mongoBackendEndpoint, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT === '' and configContext.platform === Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Hosted, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - - it("configContext.BACKEND_ENDPOINT === '' and configContext.platform !== Platform.Hosted, should return /mongoshell/indexv2.html", () => { - resetConfigContext(); - updateConfigContext({ - platform: Platform.Portal, - }); - - const endpoint = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - expect(getMongoShellUrl()).toBe(`${endpoint}/content/mongoshell/debug/index.html?${queryString}`); - }); - }); -}); - -describe("getExtensionEndpoint", () => { - it("when platform === Platform.Hosted, backendEndpoint is undefined", () => { - expect(getExtensionEndpoint(Platform.Hosted, undefined)).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint === ''", () => { - expect(getExtensionEndpoint(Platform.Hosted, "")).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint === null", () => { - expect(getExtensionEndpoint(Platform.Hosted, null)).toBe(""); - }); - - it("when platform === Platform.Hosted, backendEndpoint != ''", () => { - expect(getExtensionEndpoint(Platform.Hosted, "foo")).toBe("foo"); - }); - - it("when platform === Platform.Portal, backendEndpoint is udefined", () => { - expect(getExtensionEndpoint(Platform.Portal, undefined)).toBe(""); - }); - - it("when platform === Platform.Portal, backendEndpoint === ''", () => { - expect(getExtensionEndpoint(Platform.Portal, "")).toBe(""); - }); - - it("when platform === Platform.Portal, backendEndpoint === null", () => { - expect(getExtensionEndpoint(Platform.Portal, null)).toBe(""); - }); - - it("when platform !== Platform.Portal, backendEndpoint != ''", () => { - expect(getExtensionEndpoint(Platform.Portal, "foo")).toBe("foo"); + it("should return /index.html when useMongoProxyEndpoint is true", () => { + const useMongoProxyEndpoint: boolean = true; + expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`); }); }); diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts index a029fe440..5c4c03bdb 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts @@ -1,45 +1,13 @@ -import { configContext, Platform } from "../../../ConfigContext"; +import { configContext } from "ConfigContext"; import { userContext } from "../../../UserContext"; -export function getMongoShellUrl(): string { +export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL { const { databaseAccount: account } = userContext; const resourceId = account?.id; const accountName = account?.name; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; + const path: string = useMongoProxyEndpoint ? `/index.html?${queryString}` : `/indexv2.html?${queryString}`; - if (userContext.features.enableLegacyMongoShellV1 === true) { - return `/mongoshell/index.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV1Debug === true) { - return `/mongoshell/debug/index.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV2 === true) { - return `/mongoshell/indexv2.html?${queryString}`; - } - - if (userContext.features.enableLegacyMongoShellV2Debug === true) { - return `/mongoshell/debug/indexv2.html?${queryString}`; - } - - if (userContext.portalEnv === "localhost") { - return `/mongoshell/indexv2.html?${queryString}`; - } - - if (userContext.features.loadLegacyMongoShellFromBE === true) { - const extensionEndpoint: string = getExtensionEndpoint(configContext.platform, configContext.BACKEND_ENDPOINT); - return `${extensionEndpoint}/content/mongoshell/debug/index.html?${queryString}`; - } - - return `/mongoshell/indexv2.html?${queryString}`; -} - -export function getExtensionEndpoint(platform: string, backendEndpoint: string): string { - const runtimeEndpoint = platform === Platform.Hosted ? backendEndpoint : ""; - - const extensionEndpoint: string = backendEndpoint || runtimeEndpoint || ""; - - return extensionEndpoint; + return new URL(path, configContext.hostedExplorerURL); } diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index d3732ac1c..78281bf62 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -329,7 +329,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; if ( - ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) || + ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) || (userContext.apiType === "Cassandra" && configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) && ipRules?.length diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 7bf3c8a3f..626b855bb 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -31,11 +31,6 @@ export type Features = { readonly mongoProxyAPIs?: string; readonly enableThroughputCap: boolean; readonly enableHierarchicalKeys: boolean; - readonly enableLegacyMongoShellV1: boolean; - readonly enableLegacyMongoShellV1Debug: boolean; - readonly enableLegacyMongoShellV2: boolean; - readonly enableLegacyMongoShellV2Debug: boolean; - readonly loadLegacyMongoShellFromBE: boolean; readonly enableCopilot: boolean; readonly copilotVersion?: string; readonly disableCopilotPhoenixGateaway: boolean; @@ -43,6 +38,7 @@ export type Features = { readonly copilotChatFixedMonacoEditorHeight: boolean; readonly enablePriorityBasedExecution: boolean; readonly disableConnectionStringLogin: boolean; + readonly enableLegacyMongoShell: boolean; // can be set via both flight and feature flag autoscaleDefault: boolean; @@ -106,11 +102,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear notebooksDownBanner: "true" === get("notebooksDownBanner"), enableThroughputCap: "true" === get("enablethroughputcap"), enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"), - enableLegacyMongoShellV1: "true" === get("enablelegacymongoshellv1"), - enableLegacyMongoShellV1Debug: "true" === get("enablelegacymongoshellv1debug"), - enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"), - enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"), - loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"), enableCopilot: "true" === get("enablecopilot", "true"), copilotVersion: get("copilotversion") ?? "v2.0", disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"), @@ -118,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"), + enableLegacyMongoShell: "true" === get("enablelegacymongoshell"), }; } diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index c59db4205..f8398568c 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -82,7 +82,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = { }; export const allowedMongoProxyEndpoints: ReadonlyArray = [ - MongoProxyEndpoints.Development, + MongoProxyEndpoints.Local, MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod, MongoProxyEndpoints.Fairfax, diff --git a/web.config b/web.config index 9d9ff2619..4a967e52a 100644 --- a/web.config +++ b/web.config @@ -30,7 +30,7 @@ - + From e3fab9b5bf4d5b2527aa4a7b2fe87dbe2d60002a Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Thu, 18 Apr 2024 15:39:13 -0400 Subject: [PATCH 11/67] Add 'mongoshell' to Legacy Mongo Shell path (#1806) * LMS Mongo Proxy support * change stirng to url for get mongo shell url * fix tests * enable feature flag * fixed unit test * add mongoshell to path --------- Co-authored-by: Asier Isayas --- src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts index 5c4c03bdb..0ecbcb83e 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts @@ -7,7 +7,9 @@ export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL { const accountName = account?.name; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; - const path: string = useMongoProxyEndpoint ? `/index.html?${queryString}` : `/indexv2.html?${queryString}`; + const path: string = useMongoProxyEndpoint + ? `/mongoshell/index.html?${queryString}` + : `/mongoshell/indexv2.html?${queryString}`; return new URL(path, configContext.hostedExplorerURL); } From a5a5a95973254cdc4f255f986e4ddc11054072b9 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Fri, 19 Apr 2024 09:43:27 -0700 Subject: [PATCH 12/67] [Task 3061766] Additional Keyboard Shortcuts (#1805) * [Task 3061766] Additional Keyboard Shortcuts refmt and fix lints shortcuts for: discard, new item/sproc/udf/trigger, delete item/sproc/udf/trigger copilot shortcut * remove 'Ctrl+I' due to conflict with Monaco Autocomplete --- .../CommandBar/CommandBarComponentAdapter.tsx | 4 ++-- .../CommandBarComponentButtonFactory.tsx | 6 +++++ src/Explorer/Tabs/DocumentsTab.ts | 4 ++++ .../Tabs/QueryTab/QueryTabComponent.tsx | 7 +++--- .../StoredProcedureTabComponent.tsx | 1 + src/Explorer/Tabs/TriggerTabContent.tsx | 1 + .../Tabs/UserDefinedFunctionTabContent.tsx | 1 + src/KeyboardShortcuts.tsx | 22 ++++++++++++++++--- 8 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index eaa56591d..a5db3826c 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -41,7 +41,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const buttons = useCommandBar((state) => state.contextButtons); const isHidden = useCommandBar((state) => state.isHidden); const backgroundColor = StyleConstants.BaseLight; - const setKeyboardShortcutHandlers = useKeyboardActionHandlers((state) => state.setHandlers); + const setKeyboardActionHandlers = useKeyboardActionHandlers((state) => state.setHandlers); if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { const buttons = @@ -109,7 +109,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); - setKeyboardShortcutHandlers(keyboardHandlers); + setKeyboardActionHandlers(keyboardHandlers); return (
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 175b43d04..a1aa3e49b 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -58,6 +58,7 @@ export function createStaticCommandBarButtons( buttons.push(homeBtn); const newCollectionBtn = createNewCollectionGroup(container); + newCollectionBtn.keyboardAction = KeyboardAction.NEW_COLLECTION; // Just for the root button, not the child version we create below. buttons.push(newCollectionBtn); if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") { const addSynapseLink = createOpenSynapseLinkDialogButton(container); @@ -95,6 +96,7 @@ export function createStaticCommandBarButtons( const newStoredProcedureBtn: CommandButtonComponentProps = { iconSrc: AddStoredProcedureIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_SPROC, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); @@ -278,6 +280,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps { return { iconSrc: AddDatabaseIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_DATABASE, onCommandClick: async () => { const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; if (throughputCap && throughputCap !== -1) { @@ -340,6 +343,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newStoredProcedureBtn: CommandButtonComponentProps = { iconSrc: AddStoredProcedureIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_SPROC, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); @@ -359,6 +363,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newUserDefinedFunctionBtn: CommandButtonComponentProps = { iconSrc: AddUdfIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_UDF, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); @@ -378,6 +383,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) const newTriggerBtn: CommandButtonComponentProps = { iconSrc: AddTriggerIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_TRIGGER, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 263fa77bd..1d13237d0 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -894,6 +894,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: NewDocumentIcon, iconAlt: label, + keyboardAction: KeyboardAction.NEW_ITEM, onCommandClick: this.onNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -923,6 +924,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onRevertNewDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -953,6 +955,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onRevertExisitingDocumentClick, commandButtonLabel: label, ariaLabel: label, @@ -968,6 +971,7 @@ export default class DocumentsTab extends TabsBase { buttons.push({ iconSrc: DeleteDocumentIcon, iconAlt: label, + keyboardAction: KeyboardAction.DELETE_ITEM, onCommandClick: this.onDeleteExisitingDocumentClick, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index e3f740a25..07f0a9eba 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -440,7 +440,7 @@ export default class QueryTabComponent extends React.Component { this._toggleCopilot(!this.state.copilotActive); }, @@ -471,7 +472,7 @@ export default class QueryTabComponent extends React.Component this.queryAbortController.abort(), commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index dc179b002..ce8c6ac45 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -350,6 +350,7 @@ export default class StoredProcedureTabComponent extends React.Component< buttons.push({ iconSrc: DiscardIcon, iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, onCommandClick: this.onDiscard, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index 23ab9b2e1..5fd28502a 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -271,6 +271,7 @@ export class TriggerTabContent extends Component = { // NOTE: The "$mod" special value is used to represent the "Control" key on Windows/Linux and the "Command" key on macOS. // See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts. - [KeyboardAction.NEW_QUERY]: ["$mod+J"], + [KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"], [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"], - [KeyboardAction.CANCEL_QUERY]: ["Escape"], + [KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"], [KeyboardAction.SAVE_ITEM]: ["$mod+S"], [KeyboardAction.OPEN_QUERY]: ["$mod+O"], [KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"], + [KeyboardAction.NEW_SPROC]: ["Alt+N P"], + [KeyboardAction.NEW_UDF]: ["Alt+N F"], + [KeyboardAction.NEW_TRIGGER]: ["Alt+N T"], + [KeyboardAction.NEW_DATABASE]: ["Alt+N D"], + [KeyboardAction.NEW_COLLECTION]: ["Alt+N C"], + [KeyboardAction.NEW_ITEM]: ["Alt+N I"], + [KeyboardAction.DELETE_ITEM]: ["Alt+D"], + [KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"], }; interface KeyboardShortcutState { From c220a8b070e9037710f9d1a8e6c3ad454ce31d6c Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Fri, 19 Apr 2024 13:44:30 -0700 Subject: [PATCH 13/67] [Task 3071878] Tab Navigation Keyboard Shortcuts (#1808) * [Task 3071878] Tab Navigation Keyboard Shortcuts * throw in development on duplicate handlers * refmt --- .../CommandBar/CommandBarComponentAdapter.tsx | 6 +- src/Explorer/Tabs/Tabs.tsx | 11 ++++ src/KeyboardShortcuts.tsx | 64 +++++++++++++++++-- src/hooks/useTabs.ts | 46 +++++++++++++ 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index a5db3826c..9a5f222a3 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -5,7 +5,7 @@ */ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { useNotebook } from "Explorer/Notebook/useNotebook"; -import { useKeyboardActionHandlers } from "KeyboardShortcuts"; +import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { userContext } from "UserContext"; import * as React from "react"; import create, { UseStore } from "zustand"; @@ -41,7 +41,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const buttons = useCommandBar((state) => state.contextButtons); const isHidden = useCommandBar((state) => state.isHidden); const backgroundColor = StyleConstants.BaseLight; - const setKeyboardActionHandlers = useKeyboardActionHandlers((state) => state.setHandlers); + const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR); if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { const buttons = @@ -109,7 +109,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); - setKeyboardActionHandlers(keyboardHandlers); + setKeyboardHandlers(keyboardHandlers); return (
diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 78281bf62..b33f3d4af 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -14,6 +14,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; +import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility"; import { userContext } from "UserContext"; import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils"; @@ -42,6 +43,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { showMongoAndCassandraProxiesNetworkSettingsWarningState, setShowMongoAndCassandraProxiesNetworkSettingsWarningState, ] = useState(showMongoAndCassandraProxiesNetworkSettingsWarning()); + + const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS); + useEffect(() => { + setKeyboardHandlers({ + [KeyboardAction.SELECT_LEFT_TAB]: () => useTabs.getState().selectLeftTab(), + [KeyboardAction.SELECT_RIGHT_TAB]: () => useTabs.getState().selectRightTab(), + [KeyboardAction.CLOSE_TAB]: () => useTabs.getState().closeActiveTab(), + }); + }, [setKeyboardHandlers]); + return (
{networkSettingsWarning && ( diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index dbf6dddb7..66efd68b4 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -12,6 +12,15 @@ export type KeyboardActionHandler = (e: KeyboardEvent) => boolean | void; export type KeyboardHandlerMap = Partial>; +/** + * The groups of keyboard actions that can be managed by the application. + * Each group can be updated separately, but, when updated, must be completely replaced. + */ +export enum KeyboardActionGroup { + TABS = "TABS", + COMMAND_BAR = "COMMAND_BAR", +} + /** * The possible actions that can be triggered by keyboard shortcuts. */ @@ -30,6 +39,9 @@ export enum KeyboardAction { NEW_ITEM = "NEW_ITEM", DELETE_ITEM = "DELETE_ITEM", TOGGLE_COPILOT = "TOGGLE_COPILOT", + SELECT_LEFT_TAB = "SELECT_LEFT_TAB", + SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB", + CLOSE_TAB = "CLOSE_TAB", } /** @@ -55,6 +67,9 @@ const bindings: Record = { [KeyboardAction.NEW_ITEM]: ["Alt+N I"], [KeyboardAction.DELETE_ITEM]: ["Alt+D"], [KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"], + [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+["], + [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]"], + [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], }; interface KeyboardShortcutState { @@ -64,15 +79,47 @@ interface KeyboardShortcutState { allHandlers: KeyboardHandlerMap; /** - * Sets the keyboard shortcut handlers. + * A set of all the groups of keyboard shortcuts handlers. */ - setHandlers: (handlers: KeyboardHandlerMap) => void; + groups: Partial>; + + /** + * Sets the keyboard shortcut handlers for the given group. + */ + setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void; } -export const useKeyboardActionHandlers: UseStore = create((set) => ({ +/** + * Defines the calling component as the manager of the keyboard actions for the given group. + * @param group The group of keyboard actions to manage. + * @returns A function that can be used to set the keyboard action handlers for the given group. + */ +export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => + useKeyboardActionHandlers.getState().setHandlers(group, handlers); + +const useKeyboardActionHandlers: UseStore = create((set, get) => ({ allHandlers: {}, - setHandlers: (handlers: Partial>) => { - set({ allHandlers: handlers }); + groups: {}, + setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => { + const state = get(); + const groups = { ...state.groups, [group]: handlers }; + + // Combine all the handlers from all the groups in the correct order. + const allHandlers: KeyboardHandlerMap = {}; + eachKey(groups).forEach((group) => { + const groupHandlers = groups[group]; + if (groupHandlers) { + eachKey(groupHandlers).forEach((action) => { + // Check for duplicate handlers in development mode. + // We don't want to raise an error here in production, but having duplicate handlers is a mistake. + if (process.env.NODE_ENV === "development" && allHandlers[action]) { + throw new Error(`Duplicate handler for Keyboard Action "${action}".`); + } + allHandlers[action] = groupHandlers[action]; + }); + } + }); + set({ groups, allHandlers }); }, })); @@ -88,7 +135,7 @@ function createHandler(action: KeyboardAction): KeyboardActionHandler { } const allHandlers: KeyBindingMap = {}; -(Object.keys(bindings) as KeyboardAction[]).forEach((action) => { +eachKey(bindings).forEach((action) => { const shortcuts = bindings[action]; shortcuts.forEach((shortcut) => { allHandlers[shortcut] = createHandler(action); @@ -103,3 +150,8 @@ export function KeyboardShortcutRoot({ children }: PropsWithChildren) { return <>{children}; } + +/** A _typed_ version of `Object.keys` that preserves the original key type */ +function eachKey(record: Partial>): K[] { + return Object.keys(record) as K[]; +} diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index 10bc3b144..982768afa 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -1,3 +1,4 @@ +import { clamp } from "@fluentui/react"; import create, { UseStore } from "zustand"; import * as ViewModels from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels"; @@ -29,6 +30,11 @@ export interface TabsState { setQueryCopilotTabInitialInput: (input: string) => void; setIsTabExecuting: (state: boolean) => void; setIsQueryErrorThrown: (state: boolean) => void; + getCurrentTabIndex: () => number; + selectTabByIndex: (index: number) => void; + selectLeftTab: () => void; + selectRightTab: () => void; + closeActiveTab: () => void; } export enum ReactTabKind { @@ -175,4 +181,44 @@ export const useTabs: UseStore = create((set, get) => ({ setIsQueryErrorThrown: (state: boolean) => { set({ isQueryErrorThrown: state }); }, + getCurrentTabIndex: () => { + const state = get(); + if (state.activeReactTab !== undefined) { + return state.openedReactTabs.indexOf(state.activeReactTab); + } else if (state.activeTab !== undefined) { + const nonReactTabIndex = state.openedTabs.indexOf(state.activeTab); + if (nonReactTabIndex !== -1) { + return state.openedReactTabs.length + nonReactTabIndex; + } + } + + return -1; + }, + selectTabByIndex: (index: number) => { + const state = get(); + const totalTabCount = state.openedReactTabs.length + state.openedTabs.length; + const clampedIndex = clamp(index, totalTabCount - 1, 0); + + if (clampedIndex < state.openedReactTabs.length) { + set({ activeTab: undefined, activeReactTab: state.openedReactTabs[clampedIndex] }); + } else { + set({ activeTab: state.openedTabs[clampedIndex - state.openedReactTabs.length], activeReactTab: undefined }); + } + }, + selectLeftTab: () => { + const state = get(); + state.selectTabByIndex(state.getCurrentTabIndex() - 1); + }, + selectRightTab: () => { + const state = get(); + state.selectTabByIndex(state.getCurrentTabIndex() + 1); + }, + closeActiveTab: () => { + const state = get(); + if (state.activeReactTab !== undefined) { + state.closeReactTab(state.activeReactTab); + } else if (state.activeTab !== undefined) { + state.closeTab(state.activeTab); + } + }, })); From 2b15a4d43d05431045b750fe56bc59d44ebd182d Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:03:11 -0700 Subject: [PATCH 14/67] Update package.json (#1807) --- package-lock.json | 205 +++++++++++++++++++++++++++++++++------------- package.json | 8 +- 2 files changed, 156 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index edb334b7c..f0b65b98a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.2.1", - "@azure/ms-rest-nodeauth": "3.0.7", + "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", @@ -51,6 +51,7 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", + "@xmldom/xmldom": "0.7.13", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", "canvas": "file:./canvas", @@ -74,12 +75,14 @@ "i18next-browser-languagedetector": "6.0.1", "i18next-http-backend": "1.0.23", "iframe-resizer-react": "1.1.0", + "immer": "9.0.6", "immutable": "4.0.0-rc.12", "is-ci": "2.0.0", "jquery": "3.7.1", "jquery-typeahead": "2.11.1", "jquery-ui-dist": "1.13.2", "knockout": "3.5.1", + "loader-utils": "2.0.3", "mkdirp": "1.0.4", "monaco-editor": "0.44.0", "ms": "2.1.3", @@ -103,11 +106,12 @@ "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "sanitize-html": "2.3.3", + "shell-quote": "1.7.3", "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", "tinykeys": "2.1.0", - "underscore": "1.9.1", + "underscore": "1.12.1", "utility-types": "3.10.0", "zustand": "3.5.0" }, @@ -597,13 +601,13 @@ } }, "node_modules/@azure/ms-rest-nodeauth": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.7.tgz", - "integrity": "sha512-7Q1MyMB+eqUQy8JO+virSIzAjqR2UbKXE/YQZe+53gC8yakm8WOQ5OzGfPP+eyHqeRs6bQESyw2IC5feLWlT2A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.1.1.tgz", + "integrity": "sha512-UA/8dgLy3+ZiwJjAZHxL4MUB14fFQPkaAOZ94jsTW/Z6WmoOeny2+cLk0+dyIX/iH6qSrEWKwbStEeB970B9pA==", "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-js": "^2.0.4", - "adal-node": "^0.1.28" + "adal-node": "^0.2.2" } }, "node_modules/@azure/msal-browser": { @@ -14184,6 +14188,14 @@ } } }, + "node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -14301,29 +14313,39 @@ } }, "node_modules/adal-node": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", - "integrity": "sha512-98nQ5MQSyJR0ZY/R0Mue/cv4OkebRyKz4hS40GdkZU42Bq49ldHeup7UeAo/0vROMB57CX2et6IF0U/Pe1rY3A==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.4.tgz", + "integrity": "sha512-zIcvbwQFKMUtKxxj8YMHeTT1o/TPXfVNsTXVgXD8sxwV6h4AFQgK77dRciGhuEF9/Sdm3UQPJVPc/6XxrccSeA==", "deprecated": "This package is no longer supported. Please migrate to @azure/msal-node.", "dependencies": { - "@types/node": "^8.0.47", - "async": ">=0.6.0", + "@xmldom/xmldom": "^0.8.3", + "async": "^2.6.3", + "axios": "^0.21.1", "date-utils": "*", "jws": "3.x.x", - "request": ">= 2.52.0", "underscore": ">= 1.3.1", "uuid": "^3.1.0", - "xmldom": ">= 0.1.x", "xpath.js": "~1.1.0" }, "engines": { "node": ">= 0.6.15" } }, - "node_modules/adal-node/node_modules/@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" + "node_modules/adal-node/node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/adal-node/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } }, "node_modules/adal-node/node_modules/jwa": { "version": "1.4.1", @@ -14940,7 +14962,9 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true, + "peer": true }, "node_modules/async-hook-jl": { "version": "1.7.6", @@ -15084,6 +15108,32 @@ "webpack": ">=2" } }, + "node_modules/babel-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/babel-loader/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -21759,6 +21809,32 @@ "html-loader": "^0.5.1" } }, + "node_modules/html-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/html-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/html-minifier": { "version": "3.5.21", "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", @@ -22333,10 +22409,9 @@ } }, "node_modules/immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", - "dev": true, + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", + "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -29209,29 +29284,16 @@ } }, "node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "json5": "^2.1.2" }, "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" + "node": ">=8.9.0" } }, "node_modules/locate-path": { @@ -34255,6 +34317,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", @@ -34321,6 +34393,12 @@ "node": ">= 6" } }, + "node_modules/react-dev-utils/node_modules/shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, "node_modules/react-dev-utils/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -36410,10 +36488,9 @@ } }, "node_modules/shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" }, "node_modules/shellwords": { "version": "0.1.1", @@ -37255,6 +37332,32 @@ "node": ">= 0.12.0" } }, + "node_modules/style-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/style-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/style-loader/node_modules/schema-utils": { "version": "0.4.7", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", @@ -38443,9 +38546,9 @@ } }, "node_modules/underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, "node_modules/unherit": { "version": "1.1.3", @@ -40149,14 +40252,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "devOptional": true }, - "node_modules/xmldom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", - "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/xpath.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", diff --git a/package.json b/package.json index cc195cd5f..2c7e2b7f5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.2.1", - "@azure/ms-rest-nodeauth": "3.0.7", + "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-decorators": "7.12.12", @@ -46,6 +46,7 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", + "@xmldom/xmldom": "0.7.13", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", "canvas": "file:./canvas", @@ -69,12 +70,14 @@ "i18next-browser-languagedetector": "6.0.1", "i18next-http-backend": "1.0.23", "iframe-resizer-react": "1.1.0", + "immer": "9.0.6", "immutable": "4.0.0-rc.12", "is-ci": "2.0.0", "jquery": "3.7.1", "jquery-typeahead": "2.11.1", "jquery-ui-dist": "1.13.2", "knockout": "3.5.1", + "loader-utils": "2.0.3", "mkdirp": "1.0.4", "monaco-editor": "0.44.0", "ms": "2.1.3", @@ -98,11 +101,12 @@ "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "sanitize-html": "2.3.3", + "shell-quote": "1.7.3", "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", "tinykeys": "2.1.0", - "underscore": "1.9.1", + "underscore": "1.12.1", "utility-types": "3.10.0", "zustand": "3.5.0" }, From c12eced120fddfa4c8cea5eedce89fae72e1fc50 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 22 Apr 2024 07:10:16 -0700 Subject: [PATCH 15/67] Update node-fetch, react-dev-utils and azure/identity dependencies. (#1809) --- package-lock.json | 1125 +++++++++++++++++++++++++-------------------- package.json | 6 +- 2 files changed, 623 insertions(+), 508 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0b65b98a..590a1c049 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@azure/arm-cosmosdb": "9.1.0", "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", - "@azure/identity": "1.2.1", + "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", @@ -180,13 +180,13 @@ "less-vars-loader": "1.1.0", "mini-css-extract-plugin": "2.1.0", "monaco-editor-webpack-plugin": "7.1.0", - "node-fetch": "2.6.1", + "node-fetch": "2.6.7", "playwright": "1.13.0", "prettier": "3.0.3", "process": "0.11.10", "querystring-es3": "0.2.1", "raw-loader": "0.5.1", - "react-dev-utils": "11.0.4", + "react-dev-utils": "12.0.1", "rimraf": "3.0.0", "sinon": "3.2.1", "style-loader": "0.23.0", @@ -253,14 +253,6 @@ "tslib": "^1.10.0" } }, - "node_modules/@azure/core-asynciterator-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz", - "integrity": "sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw==", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/@azure/core-auth": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", @@ -279,66 +271,35 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/@azure/core-http": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", - "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-asynciterator-polyfill": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", "@azure/logger": "^1.0.0", - "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.1", - "form-data": "^3.0.0", - "node-fetch": "^2.6.0", - "process": "^0.11.10", - "tough-cookie": "^4.0.0", - "tslib": "^2.2.0", - "tunnel": "^0.0.6", - "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-http/node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.11", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", - "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dependencies": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "1.0.0-rc.0", - "tslib": "^2.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-http/node_modules/@opentelemetry/api": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", - "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@azure/core-http/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@azure/core-http/node_modules/tslib": { + "node_modules/@azure/core-client/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" @@ -459,42 +420,44 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/identity": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.2.1.tgz", - "integrity": "sha512-vCzV4Xg5hWJ2e4Et0waOmIEgYHsqtGF06kklnqblZg0hKDLKxTAX5FzKYuDMk1CctY2UdEmWFcA2li2uOXOLXQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.5.2.tgz", + "integrity": "sha512-vqyeRbd2i0h9F4mqW5JbkP1xfabqKQ21l/81osKhpOQ2LtwaJW6nw4+0PsVYnxcbPHFCIZt6EWAk74a3OGYZJA==", "dependencies": { - "@azure/core-http": "^1.2.0", - "@azure/core-tracing": "1.0.0-preview.9", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "1.0.0-preview.12", "@azure/logger": "^1.0.0", - "@azure/msal-node": "1.0.0-beta.1", - "@opentelemetry/api": "^0.10.2", + "@azure/msal-node": "1.0.0-beta.6", + "@types/stoppable": "^1.1.0", "axios": "^0.21.1", "events": "^3.0.0", "jws": "^4.0.0", "msal": "^1.0.2", "open": "^7.0.0", "qs": "^6.7.0", + "stoppable": "^1.1.0", "tslib": "^2.0.0", "uuid": "^8.3.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" }, "optionalDependencies": { - "keytar": "^5.4.0" + "keytar": "^7.3.0" } }, "node_modules/@azure/identity/node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.9", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz", - "integrity": "sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==", + "version": "1.0.0-preview.12", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.12.tgz", + "integrity": "sha512-nvo2Wc4EKZGN6eFu9n3U7OXmASmL8VxoPIH7xaD6OlQqi44bouF0YIi9ID5rEsKLiAU59IYx6M297nqWVMWPDg==", "dependencies": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "^0.10.2", - "tslib": "^2.0.0" + "@opentelemetry/api": "^1.0.0", + "tslib": "^2.2.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" } }, "node_modules/@azure/identity/node_modules/tslib": { @@ -634,61 +597,17 @@ } }, "node_modules/@azure/msal-node": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.1.tgz", - "integrity": "sha512-dO/bgVScpl5loZfsfhHXmFLTNoDxGvUiZIsJCe1+HpHyFWXwGsBZ71P5ixbxRhhf/bPpZS3X+/rm1Fq2uUucJw==", + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.0.0-beta.6.tgz", + "integrity": "sha512-ZQI11Uz1j0HJohb9JZLRD8z0moVcPks1AFW4Q/Gcl67+QvH4aKEJti7fjCcipEEZYb/qzLSO8U6IZgPYytsiJQ==", "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", "dependencies": { - "@azure/msal-common": "^1.7.2", - "axios": "^0.19.2", + "@azure/msal-common": "^4.0.0", + "axios": "^0.21.1", "jsonwebtoken": "^8.5.1", "uuid": "^8.3.0" } }, - "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-1.7.2.tgz", - "integrity": "sha512-3/voCdFKONENX+5tMrNOBSrVJb6NbE7YB8vc4FZ/4ZbjpK7GVtq9Bu1MW+HZhrmsUzSF/joHx0ZIJDYIequ/jg==", - "dependencies": { - "debug": "^4.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node/node_modules/axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", - "dependencies": { - "follow-redirects": "1.5.10" - } - }, - "node_modules/@azure/msal-node/node_modules/follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "dependencies": { - "debug": "=3.1.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@azure/msal-node/node_modules/follow-redirects/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/@azure/msal-node/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/@babel/code-frame": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", @@ -7071,17 +6990,6 @@ "node": ">=10" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@mapbox/node-pre-gyp/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -10184,29 +10092,10 @@ "@octokit/openapi-types": "^19.0.2" } }, - "node_modules/@opencensus/web-types": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", - "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==", - "engines": { - "node": ">=6.0" - } - }, "node_modules/@opentelemetry/api": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz", - "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", - "dependencies": { - "@opentelemetry/context-base": "^0.10.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/context-base": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", - "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", "engines": { "node": ">=8.0.0" } @@ -12839,6 +12728,12 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "devOptional": true }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, "node_modules/@types/post-robot": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@types/post-robot/-/post-robot-10.0.1.tgz", @@ -13618,6 +13513,14 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "devOptional": true }, + "node_modules/@types/stoppable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.3.tgz", + "integrity": "sha512-7wGKIBJGE4ZxFjk9NkjAxZMLlIXroETqP1FJCdoSvKmEznwmBxQFmTB1dsCkAvVcNemuSZM5qkkd9HE/NL2JTw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/styled-components": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.1.tgz", @@ -13644,14 +13547,6 @@ "@types/jest": "*" } }, - "node_modules/@types/tunnel": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", - "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/underscore": { "version": "1.7.36", "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.7.36.tgz", @@ -14661,7 +14556,9 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/archy": { "version": "1.0.0", @@ -14669,16 +14566,6 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -16469,7 +16356,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16746,7 +16635,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -17020,7 +16911,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "devOptional": true + "dev": true }, "node_modules/cosmiconfig": { "version": "5.2.1", @@ -17861,7 +17752,9 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "mimic-response": "^2.0.0" }, @@ -18150,7 +18043,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/denodeify": { "version": "1.2.1", @@ -18204,15 +18099,12 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/detect-newline": { @@ -20325,9 +20217,9 @@ "optional": true }, "node_modules/filesize": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", - "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", "dev": true, "engines": { "node": ">= 0.4.0" @@ -20725,22 +20617,200 @@ } }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", - "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.5.5", - "chalk": "^2.4.1", - "micromatch": "^3.1.10", + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", "minimatch": "^3.0.4", - "semver": "^5.6.0", - "tapable": "^1.0.0", - "worker-rpc": "^0.1.0" + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" }, "engines": { - "node": ">=6.11.5", + "node": ">=10", "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { @@ -20752,6 +20822,30 @@ "node": ">=6" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -20987,43 +21081,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", - "optional": true, - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "optional": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -21375,16 +21432,18 @@ "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" }, "node_modules/gzip-size": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", - "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, "dependencies": { - "duplexer": "^0.1.1", - "pify": "^4.0.1" + "duplexer": "^0.1.2" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/handle-thing": { @@ -21520,7 +21579,9 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/has-value": { "version": "1.0.0", @@ -22314,6 +22375,14 @@ "node-fetch": "2.6.1" } }, + "node_modules/i18next-http-backend/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -22863,7 +22932,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "number-is-nan": "^1.0.0" }, @@ -28995,22 +29066,16 @@ "integrity": "sha512-yQa1dz+FilQ+w3JM6GH2V/wnFeQhfbkK9stvs3UiraW3GOEO7zrOBBh0ZuHsrzeN1xx6v7P5EpA2JtOUUnfN/w==" }, "node_modules/keytar": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-5.6.0.tgz", - "integrity": "sha512-ueulhshHSGoryfRXaIvTj0BV1yB0KddBGhGoqCxSN9LR1Ks1GKuuCdVhF+2/YOs5fMl6MlTI9On1a4DHDXoTow==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "hasInstallScript": true, "optional": true, "dependencies": { - "nan": "2.14.1", - "prebuild-install": "5.3.3" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, - "node_modules/keytar/node_modules/nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "optional": true - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -31389,12 +31454,6 @@ "node": ">=12" } }, - "node_modules/microevent.ts": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", - "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", - "dev": true - }, "node_modules/micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -31470,7 +31529,9 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -31885,19 +31946,61 @@ } }, "node_modules/node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.60.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.60.0.tgz", + "integrity": "sha512-zcGgwoXbzw9NczqbGzAWL/ToDYAxv1V8gL1D67ClbdkIfeeDBbY0GelZtC25ayLvVjr2q2cloHeQV1R0QAWqRQ==", "optional": true, "dependencies": { - "semver": "^5.4.1" + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" } }, + "node_modules/node-abi/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "optional": true + }, "node_modules/node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -31912,11 +32015,22 @@ } }, "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-forge": { @@ -32015,12 +32129,6 @@ "url": "https://github.com/sponsors/antelle" } }, - "node_modules/noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", - "optional": true - }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -32074,18 +32182,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -32108,7 +32204,9 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -33582,44 +33680,81 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prebuild-install": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz", - "integrity": "sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" + "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/prebuild-install/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/prebuild-install/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "optional": true, "dependencies": { - "minimist": "^1.2.6" + "mimic-response": "^3.1.0" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, "node_modules/prelude-ls": { @@ -33803,7 +33938,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "devOptional": true + "dev": true }, "node_modules/process-on-spawn": { "version": "1.0.0", @@ -34210,106 +34345,110 @@ } }, "node_modules/react-dev-utils": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", - "integrity": "sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", "dev": true, "dependencies": { - "@babel/code-frame": "7.10.4", - "address": "1.1.2", - "browserslist": "4.14.2", - "chalk": "2.4.2", - "cross-spawn": "7.0.3", - "detect-port-alt": "1.1.6", - "escape-string-regexp": "2.0.0", - "filesize": "6.1.0", - "find-up": "4.1.0", - "fork-ts-checker-webpack-plugin": "4.1.6", - "global-modules": "2.0.0", - "globby": "11.0.1", - "gzip-size": "5.1.1", - "immer": "8.0.1", - "is-root": "2.1.0", - "loader-utils": "2.0.0", - "open": "^7.0.2", - "pkg-up": "3.1.0", - "prompts": "2.4.0", - "react-error-overlay": "^6.0.9", - "recursive-readdir": "2.2.2", - "shell-quote": "1.7.2", - "strip-ansi": "6.0.0", - "text-table": "0.2.0" + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/react-dev-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-dev-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" - } - }, - "node_modules/react-dev-utils/node_modules/@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/react-dev-utils/node_modules/browserslist": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.2.tgz", - "integrity": "sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001125", - "electron-to-chromium": "^1.3.564", - "escalade": "^3.0.2", - "node-releases": "^1.1.61" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" }, "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/react-dev-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/react-dev-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/react-dev-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-dev-utils/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - }, "engines": { "node": ">=10" }, @@ -34317,10 +34456,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/react-dev-utils/node_modules/immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "dev": true, "funding": { "type": "opencollective", @@ -34328,47 +34476,74 @@ } }, "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, "engines": { - "node": ">=8.9.0" + "node": ">= 12.13.0" } }, "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-dev-utils/node_modules/node-releases": { - "version": "1.1.77", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz", - "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==", - "dev": true - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/react-dev-utils/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-dev-utils/node_modules/path-exists": { @@ -34380,41 +34555,13 @@ "node": ">=8" } }, - "node_modules/react-dev-utils/node_modules/prompts": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", - "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "node_modules/react-dev-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/react-dev-utils/node_modules/shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true - }, - "node_modules/react-dev-utils/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=8" @@ -34956,7 +35103,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "devOptional": true, + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -34971,7 +35118,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "devOptional": true + "dev": true }, "node_modules/readdirp": { "version": "3.6.0", @@ -36560,7 +36707,9 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -37129,6 +37278,15 @@ "node": ">=0.10.0" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -37161,7 +37319,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -37175,7 +37335,9 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -37184,7 +37346,9 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -37994,6 +38158,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "devOptional": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -38008,6 +38173,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "devOptional": true, "engines": { "node": ">= 4.0.0" } @@ -39373,21 +39539,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-bundle-analyzer/node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", @@ -39893,15 +40044,6 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "optional": true, - "engines": { - "node": ">=4" - } - }, "node_modules/which-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", @@ -39924,7 +40066,9 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "optional": true, + "peer": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -40063,15 +40207,6 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, - "node_modules/worker-rpc": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", - "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", - "dev": true, - "dependencies": { - "microevent.ts": "~0.1.1" - } - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -40217,26 +40352,6 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", diff --git a/package.json b/package.json index 2c7e2b7f5..9ac61d243 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@azure/arm-cosmosdb": "9.1.0", "@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos-language-service": "0.0.5", - "@azure/identity": "1.2.1", + "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", "@azure/msal-browser": "2.14.2", "@babel/plugin-proposal-class-properties": "7.12.1", @@ -175,13 +175,13 @@ "less-vars-loader": "1.1.0", "mini-css-extract-plugin": "2.1.0", "monaco-editor-webpack-plugin": "7.1.0", - "node-fetch": "2.6.1", + "node-fetch": "2.6.7", "playwright": "1.13.0", "prettier": "3.0.3", "process": "0.11.10", "querystring-es3": "0.2.1", "raw-loader": "0.5.1", - "react-dev-utils": "11.0.4", + "react-dev-utils": "12.0.1", "rimraf": "3.0.0", "sinon": "3.2.1", "style-loader": "0.23.0", From 7b81767ded7031167d57f1ae7f2075d8bd9eaad0 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:34:20 -0700 Subject: [PATCH 16/67] Enable new backend for Settings API in Prod. (#1791) --- src/Utils/EndpointUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index f8398568c..e4c036d75 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -155,7 +155,11 @@ export function useNewPortalBackendEndpoint(backendApi: string): boolean { // This maps backend APIs to the environments supported by the new backend. const newBackendApiEnvironmentMap: { [key: string]: string[] } = { [BackendApi.GenerateToken]: [PortalBackendEndpoints.Development], - [BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac], + [BackendApi.PortalSettings]: [ + PortalBackendEndpoints.Development, + PortalBackendEndpoints.Mpac, + PortalBackendEndpoints.Prod, + ], }; if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) { From acf5acfdb45c0609b8dbc83cddfb730ec3a25b66 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Tue, 23 Apr 2024 08:20:27 -0400 Subject: [PATCH 17/67] Remove Legacy Mongo Shell feature flag (#1810) * LMS Mongo Proxy support * change stirng to url for get mongo shell url * fix tests * enable feature flag * fixed unit test * add mongoshell to path * remove LMS feature flag --------- Co-authored-by: Asier Isayas --- src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx | 4 ++-- src/Platform/Hosted/extractFeatures.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index a89ca5fc7..d3720cf41 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -1,3 +1,4 @@ +import { useMongoProxyEndpoint } from "Common/MongoProxyClient"; import React, { Component } from "react"; import * as Constants from "../../../Common/Constants"; import { configContext } from "../../../ConfigContext"; @@ -54,8 +55,7 @@ export default class MongoShellTabComponent extends Component< constructor(props: IMongoShellTabComponentProps) { super(props); this._logTraces = new Map(); - this._useMongoProxyEndpoint = userContext.features.enableLegacyMongoShell; - // this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell"); + this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell"); this.state = { url: getMongoShellUrl(this._useMongoProxyEndpoint), diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 626b855bb..5bd84516e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -38,7 +38,6 @@ export type Features = { readonly copilotChatFixedMonacoEditorHeight: boolean; readonly enablePriorityBasedExecution: boolean; readonly disableConnectionStringLogin: boolean; - readonly enableLegacyMongoShell: boolean; // can be set via both flight and feature flag autoscaleDefault: boolean; @@ -109,7 +108,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"), - enableLegacyMongoShell: "true" === get("enablelegacymongoshell"), }; } From c1a28793ba04c000b29af923051dff3435118bad Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 23 Apr 2024 09:08:29 -0700 Subject: [PATCH 18/67] bind F5 to execute query (#1813) --- src/KeyboardShortcuts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 66efd68b4..023b7078e 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -54,7 +54,7 @@ const bindings: Record = { // See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts. [KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"], - [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"], + [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter", "F5"], [KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"], [KeyboardAction.SAVE_ITEM]: ["$mod+S"], [KeyboardAction.OPEN_QUERY]: ["$mod+O"], From d36e511b1873403a742bcdae1fd96f6963df9bdf Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:15:48 -0700 Subject: [PATCH 19/67] Update d3, webpack-dev-server, typedoc dependencies. (#1812) * Update d3, webpack-dev-server, typedoc dependencies. * Fix unit test failures. * Revert change to snapshot as it doesn't seem required when running in github. --- jest.config.js | 5 +- package-lock.json | 582 +++++++++++++++++++++++++++++++--------------- package.json | 6 +- 3 files changed, 405 insertions(+), 188 deletions(-) diff --git a/jest.config.js b/jest.config.js index c00efdac6..b4f660063 100644 --- a/jest.config.js +++ b/jest.config.js @@ -76,6 +76,10 @@ module.exports = { "^dnd-core$": "dnd-core/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs", "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", + "d3-force": "/node_modules/d3-force/dist/d3-force.min.js", + "d3-quadtree": "/node_modules/d3-quadtree/dist/d3-quadtree.min.js", + "d3-scale-chromatic": "/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js", + "d3-zoom": "/node_modules/d3-zoom/dist/d3-zoom.min.js", }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader @@ -130,7 +134,6 @@ module.exports = { // The test environment that will be used for testing // testEnvironment: "jest-environment-jsdom", - modulePaths: ["node_modules", "/src"], // Options that will be passed to the testEnvironment diff --git a/package-lock.json b/package-lock.json index 590a1c049..8dcfe5d50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "copy-webpack-plugin": "11.0.0", "crossroads": "0.12.2", "css-element-queries": "1.1.1", - "d3": "6.1.1", + "d3": "7.8.5", "datatables.net-colreorder-dt": "1.7.0", "datatables.net-dt": "1.13.8", "date-fns": "1.29.0", @@ -191,14 +191,14 @@ "sinon": "3.2.1", "style-loader": "0.23.0", "ts-loader": "9.2.4", - "typedoc": "0.21.5", + "typedoc": "0.22.15", "typescript": "4.3.5", "url-loader": "4.1.1", "wait-on": "4.0.2", "webpack": "5.88.2", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1" + "webpack-dev-server": "4.15.2" } }, "canvas": { @@ -17259,40 +17259,43 @@ } }, "node_modules/d3": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/d3/-/d3-6.1.1.tgz", - "integrity": "sha512-bJYW9wlS2uvP2EoMkcPptrUzLMHQKCbiSW+/la8iGSLZgs4KbI/f3Fch4RtnUA9PA+/nPlwyFYzTwDjX80Of8w==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", "dependencies": { - "d3-array": "2", - "d3-axis": "2", - "d3-brush": "2", - "d3-chord": "2", - "d3-color": "2", - "d3-contour": "2", - "d3-delaunay": "5", - "d3-dispatch": "2", - "d3-drag": "2", - "d3-dsv": "2", - "d3-ease": "2", - "d3-fetch": "2", - "d3-force": "2", - "d3-format": "2", - "d3-geo": "2", - "d3-hierarchy": "2", - "d3-interpolate": "2", - "d3-path": "2", - "d3-polygon": "2", - "d3-quadtree": "2", - "d3-random": "2", - "d3-scale": "3", - "d3-scale-chromatic": "2", - "d3-selection": "2", - "d3-shape": "2", - "d3-time": "2", - "d3-time-format": "3", - "d3-timer": "2", - "d3-transition": "2", - "d3-zoom": "2" + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-array": { @@ -17304,9 +17307,12 @@ } }, "node_modules/d3-axis": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.1.0.tgz", - "integrity": "sha512-z/G2TQMyuf0X3qP+Mh+2PimoJD41VOCjViJzT0BHeL/+JQAofkiWZbWxlwFGb1N8EN+Cl/CW+MUKbVzr1689Cw==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-brush": { "version": "2.1.0", @@ -17321,11 +17327,14 @@ } }, "node_modules/d3-chord": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-2.0.0.tgz", - "integrity": "sha512-D5PZb7EDsRNdGU4SsjQyKhja8Zgu+SHZfUSO5Ls8Wsn+jsAKUUGkcshLxMg9HDFxG3KqavGWaWkJ8EpU8ojuig==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "dependencies": { - "d3-path": "1 - 2" + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-collection": { @@ -17339,19 +17348,36 @@ "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" }, "node_modules/d3-contour": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-2.0.0.tgz", - "integrity": "sha512-9unAtvIaNk06UwqBmvsdHX7CZ+NPDZnn8TtNH1myW93pWJkhsV25JcgnYAu0Ck5Veb1DHiCv++Ic5uvJ+h50JA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "dependencies": { - "d3-array": "2" + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-delaunay": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz", - "integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "dependencies": { - "delaunator": "4" + "delaunator": "5" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-dispatch": { @@ -17369,24 +17395,46 @@ } }, "node_modules/d3-dsv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz", - "integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "dependencies": { - "commander": "2", - "iconv-lite": "0.4", + "commander": "7", + "iconv-lite": "0.6", "rw": "1" }, "bin": { - "csv2json": "bin/dsv2json", - "csv2tsv": "bin/dsv2dsv", - "dsv2dsv": "bin/dsv2dsv", - "dsv2json": "bin/dsv2json", - "json2csv": "bin/json2dsv", - "json2dsv": "bin/json2dsv", - "json2tsv": "bin/json2dsv", - "tsv2csv": "bin/dsv2dsv", - "tsv2json": "bin/dsv2json" + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/d3-ease": { @@ -17395,21 +17443,27 @@ "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==" }, "node_modules/d3-fetch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-2.0.0.tgz", - "integrity": "sha512-TkYv/hjXgCryBeNKiclrwqZH7Nb+GaOwo3Neg24ZVWA3MKB+Rd+BY84Nh6tmNEMcjUik1CSUWjXYndmeO6F7sw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "dependencies": { - "d3-dsv": "1 - 2" + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-force": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", - "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "dependencies": { - "d3-dispatch": "1 - 2", - "d3-quadtree": "1 - 2", - "d3-timer": "1 - 2" + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-format": { @@ -17418,11 +17472,14 @@ "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==" }, "node_modules/d3-geo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", - "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "dependencies": { - "d3-array": "^2.5.0" + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-glyphedge": { @@ -17436,9 +17493,12 @@ "integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==" }, "node_modules/d3-hierarchy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz", - "integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-interpolate": { "version": "2.0.1", @@ -17449,9 +17509,12 @@ } }, "node_modules/d3-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", - "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-path-arrows": { "version": "0.4.0", @@ -17473,19 +17536,28 @@ "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" }, "node_modules/d3-polygon": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", - "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-quadtree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz", - "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-random": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz", - "integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-sankey-circular": { "version": "0.25.0", @@ -17515,12 +17587,15 @@ } }, "node_modules/d3-scale-chromatic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz", - "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "dependencies": { - "d3-color": "1 - 2", - "d3-interpolate": "1 - 2" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-selection": { @@ -17583,23 +17658,181 @@ "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" }, "node_modules/d3-zoom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz", - "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "dependencies": { - "d3-dispatch": "1 - 2", - "d3-drag": "2", - "d3-interpolate": "1 - 2", - "d3-selection": "2", - "d3-transition": "2" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" } }, "node_modules/d3/node_modules/d3-shape": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", - "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dependencies": { - "d3-path": "1 - 2" + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, "node_modules/dashdash": { @@ -18021,9 +18254,12 @@ } }, "node_modules/delaunator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", - "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -21452,36 +21688,6 @@ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -28937,9 +29143,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, "node_modules/jsonfile": { @@ -29740,15 +29946,15 @@ } }, "node_modules/marked": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", - "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "bin": { - "marked": "bin/marked" + "marked": "bin/marked.js" }, "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/martinez-polygon-clipping": { @@ -35900,6 +36106,11 @@ "rimraf": "bin.js" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -36645,9 +36856,9 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, "node_modules/shiki": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.15.tgz", - "integrity": "sha512-/Y0z9IzhJ8nD9nbceORCqu6NgT9X6I8Fk8c3SICHI5NbZRLdZYFaB233gwct9sU0vvSypyaL/qaKvzyQGJBZSw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz", + "integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==", "dev": true, "dependencies": { "jsonc-parser": "^3.0.0", @@ -38572,19 +38783,16 @@ } }, "node_modules/typedoc": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.21.5.tgz", - "integrity": "sha512-uRDRmYheE5Iju9Zz0X50pTASTpBorIHFt02F5Y8Dt4eBt55h3mwk1CBSY2+EfwBxY16N4Xm7f8KXhnfFZ0AmBw==", + "version": "0.22.15", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.15.tgz", + "integrity": "sha512-CMd1lrqQbFvbx6S9G6fL4HKp3GoIuhujJReWqlIvSb2T26vGai+8Os3Mde7Pn832pXYemd9BMuuYWhFpL5st0Q==", "dev": true, "dependencies": { - "glob": "^7.1.7", - "handlebars": "^4.7.7", + "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^2.1.1", - "minimatch": "^3.0.0", - "progress": "^2.0.3", - "shiki": "^0.9.3", - "typedoc-default-themes": "^0.12.10" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" }, "bin": { "typedoc": "bin/typedoc" @@ -38593,16 +38801,28 @@ "node": ">= 12.10.0" }, "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x" + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x" } }, - "node_modules/typedoc-default-themes": { - "version": "0.12.10", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz", - "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==", + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">= 8" + "node": ">=10" } }, "node_modules/typescript": { @@ -39615,9 +39835,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -39691,9 +39911,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -39724,7 +39944,7 @@ "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", + "webpack-dev-middleware": "^5.3.4", "ws": "^8.13.0" }, "bin": { @@ -40201,12 +40421,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index 9ac61d243..3a2f8b742 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "copy-webpack-plugin": "11.0.0", "crossroads": "0.12.2", "css-element-queries": "1.1.1", - "d3": "6.1.1", + "d3": "7.8.5", "datatables.net-colreorder-dt": "1.7.0", "datatables.net-dt": "1.13.8", "date-fns": "1.29.0", @@ -186,14 +186,14 @@ "sinon": "3.2.1", "style-loader": "0.23.0", "ts-loader": "9.2.4", - "typedoc": "0.21.5", + "typedoc": "0.22.15", "typescript": "4.3.5", "url-loader": "4.1.1", "wait-on": "4.0.2", "webpack": "5.88.2", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1" + "webpack-dev-server": "4.15.2" }, "scripts": { "postinstall": "patch-package", From 17207624a9e20d1d5d77ff0916f8176d9eca65f5 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 23 Apr 2024 15:46:41 -0700 Subject: [PATCH 20/67] add more intl-friendly tab nav shortcuts (#1814) --- src/KeyboardShortcuts.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 023b7078e..5ce91b8f2 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -67,8 +67,8 @@ const bindings: Record = { [KeyboardAction.NEW_ITEM]: ["Alt+N I"], [KeyboardAction.DELETE_ITEM]: ["Alt+D"], [KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"], - [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+["], - [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]"], + [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"], + [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"], [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], }; From f4bcee54612b4821176609fec170e135d03e4ec7 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 23 Apr 2024 15:47:04 -0700 Subject: [PATCH 21/67] initialize new documents with their partition key (#1815) * initialize new documents with their partition key * refmt --- src/Explorer/Tabs/DocumentsTab.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 1d13237d0..c79f7eb4f 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -463,7 +463,22 @@ export default class DocumentsTab extends TabsBase { private initializeNewDocument = (): void => { this.selectedDocumentId(null); - const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); + const newDocument: any = { + id: "replace_with_new_document_id", + }; + this.partitionKeyProperties.forEach((partitionKeyProperty) => { + let target = newDocument; + const keySegments = partitionKeyProperty.split("."); + const finalSegment = keySegments.pop(); + + // Initialize nested objects as needed + keySegments.forEach((segment) => { + target = target[segment] = target[segment] || {}; + }); + + target[finalSegment] = "replace_with_new_partition_key_value"; + }); + const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4); this.initialDocumentContent(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); From afc82845b5a5eb5fe4367400f6423f1a5a3c60d2 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Wed, 24 Apr 2024 15:04:01 -0400 Subject: [PATCH 22/67] activate Token Controller (#1820) Co-authored-by: Asier Isayas --- src/Utils/EndpointUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index e4c036d75..97e733b98 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -154,7 +154,11 @@ export const allowedNotebookServerUrls: ReadonlyArray = []; export function useNewPortalBackendEndpoint(backendApi: string): boolean { // This maps backend APIs to the environments supported by the new backend. const newBackendApiEnvironmentMap: { [key: string]: string[] } = { - [BackendApi.GenerateToken]: [PortalBackendEndpoints.Development], + [BackendApi.GenerateToken]: [ + PortalBackendEndpoints.Development, + PortalBackendEndpoints.Mpac, + PortalBackendEndpoints.Prod, + ], [BackendApi.PortalSettings]: [ PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac, From 618c5ec0fee67545eb53bc471cb45449972b4ff2 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 24 Apr 2024 15:11:51 -0700 Subject: [PATCH 23/67] Add button (and keyboard shortcut) to download query (#1817) --- .../Tabs/QueryTab/QueryTabComponent.tsx | 46 +++++++++++++++---- src/KeyboardShortcuts.tsx | 2 + 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 07f0a9eba..d5f4bf697 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -22,6 +22,7 @@ import "react-splitter-layout/lib/index.css"; import { format } from "react-string-format"; import QueryCommandIcon from "../../../../images/CopilotCommand.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; +import DownloadQueryIcon from "../../../../images/DownloadQuery.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg"; @@ -225,6 +226,20 @@ export default class QueryTabComponent extends React.Component { + const text = this.getCurrentEditorQuery(); + const queryFile = new File([text], `SavedQuery.txt`, { type: "text/plain" }); + + // It appears the most consistent to download a file from a blob is to create an anchor element and simulate clicking it + const blobUrl = URL.createObjectURL(queryFile); + const anchor = document.createElement("a"); + anchor.href = blobUrl; + anchor.download = queryFile.name; + document.body.appendChild(anchor); // Must put the anchor in the document. + anchor.click(); + document.body.removeChild(anchor); // Clean up the anchor. + }; + public onSaveQueryClick = (): void => { useSidePanel.getState().openSidePanel("Save Query", ); }; @@ -405,15 +420,28 @@ export default class QueryTabComponent extends React.Component 0; + useCommandBar.getState().setContextButtons(this.getTabsButtons()); } diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 5ce91b8f2..98f988038 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -29,6 +29,7 @@ export enum KeyboardAction { EXECUTE_ITEM = "EXECUTE_ITEM", CANCEL_OR_DISCARD = "CANCEL_OR_DISCARD", SAVE_ITEM = "SAVE_ITEM", + DOWNLOAD_ITEM = "DOWNLOAD_ITEM", OPEN_QUERY = "OPEN_QUERY", OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK", NEW_SPROC = "NEW_SPROC", @@ -57,6 +58,7 @@ const bindings: Record = { [KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter", "F5"], [KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"], [KeyboardAction.SAVE_ITEM]: ["$mod+S"], + [KeyboardAction.DOWNLOAD_ITEM]: ["$mod+Shift+S"], [KeyboardAction.OPEN_QUERY]: ["$mod+O"], [KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"], [KeyboardAction.NEW_SPROC]: ["Alt+N P"], From cbd5e6bf761b5469c63712ec6a451122e3dddcc1 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Fri, 26 Apr 2024 14:55:47 -0400 Subject: [PATCH 24/67] open Legacy Mongo SHell with correct base URL in sovereign clouds (#1823) Co-authored-by: Asier Isayas --- .../Tabs/MongoShellTab/MongoShellTabComponent.tsx | 4 ++-- src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx index d3720cf41..052c81ce6 100644 --- a/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx +++ b/src/Explorer/Tabs/MongoShellTab/MongoShellTabComponent.tsx @@ -35,7 +35,7 @@ export interface IMongoShellTabAccessor { } export interface IMongoShellTabComponentStates { - url: URL; + url: string; } export interface IMongoShellTabComponentProps { @@ -221,7 +221,7 @@ export default class MongoShellTabComponent extends Component< name="explorer" className="iframe" style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }} - src={this.state.url.toString()} + src={this.state.url} id={this.props.tabsBaseInstance.tabId} onLoad={(event) => this.setContentFocus(event)} title="Mongo Shell" diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts index 0ecbcb83e..a3b49b373 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.ts @@ -1,15 +1,11 @@ -import { configContext } from "ConfigContext"; import { userContext } from "../../../UserContext"; -export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL { +export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string { const { databaseAccount: account } = userContext; const resourceId = account?.id; const accountName = account?.name; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; - const path: string = useMongoProxyEndpoint - ? `/mongoshell/index.html?${queryString}` - : `/mongoshell/indexv2.html?${queryString}`; - return new URL(path, configContext.hostedExplorerURL); + return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`; } From f8f7ea34bdd156c8e7fd04c9945ae93652942d74 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:09:55 -0500 Subject: [PATCH 25/67] Copilot rewording (#1824) * Copilot rebranding to query advisor * fix the subquery link --- src/Explorer/Panes/SettingsPane/SettingsPane.tsx | 4 ++-- src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx | 2 +- src/Explorer/QueryCopilot/QueryCopilotTab.tsx | 6 +++--- src/Explorer/SplashScreen/SplashScreen.tsx | 4 ++-- src/Explorer/Tabs/QueryTab/QueryResultSection.tsx | 10 +++++++--- src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx | 6 +++--- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 1f0eda4f7..9b2412c4b 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -630,7 +630,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ Enable sample database This is a sample database and collection with synthetic product data you can use to explore using - NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is + NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is created by, and maintained by Microsoft at no cost to you.
@@ -640,7 +640,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ label: { padding: 0 }, }} className="padding" - ariaLabel="Enable sample db for Copilot" + ariaLabel="Enable sample db for Query Advisor" checked={copilotSampleDBEnabled} onChange={handleSampleDatabaseChange} /> diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index 6aaebc926..6c54cd506 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -385,7 +385,7 @@ export const QueryCopilotPromptbar: React.FC = ({ hasSmallHeadline={true} headline="Write a prompt" > - Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} + Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "} { setShowSamplePrompts(true); diff --git a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx index 240881310..150f7ec5b 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx @@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC = ({ explorer }: Query const toggleCopilotButton = { iconSrc: QueryCommandIcon, - iconAlt: "Copilot", + iconAlt: "Query Advisor", onCommandClick: () => { toggleCopilot(true); }, - commandButtonLabel: "Copilot", - ariaLabel: "Copilot", + commandButtonLabel: "Query Advisor", + ariaLabel: "Query Advisor", hasPopup: false, disabled: copilotActive, }; diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index aa7e7dcd9..f4ebb9cd0 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -151,9 +151,9 @@ export class SplashScreen extends React.Component { {useQueryCopilot.getState().copilotEnabled && ( { const copilotVersion = userContext.features.copilotVersion; diff --git a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx index fc93d98a1..30d2ed9c1 100644 --- a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx @@ -381,9 +381,13 @@ export const QueryResultSection: React.FC = ({ Error - We have detected you may be using a subquery. Non-correlated subqueries are not currently supported. - - Please see Cosmos sub query documentation for further information + We detected you may be using a subquery. To learn more about subqueries effectively,{" "} + + visit the documentation
diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index d5f4bf697..532ce4662 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -483,13 +483,13 @@ export default class QueryTabComponent extends React.Component { this._toggleCopilot(!this.state.copilotActive); }, - commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", - ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot", + commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", + ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", hasPopup: false, }; buttons.push(toggleCopilotButton); From b94ce28e96edd5b65896dec56a6baf833fba63cd Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Mon, 29 Apr 2024 22:23:49 +0530 Subject: [PATCH 26/67] [accessibility-2724013]:[Screen reader - Cosmos DB - Data Explorer -> Entities -> Add entity]: Screen reader announces incorrect role when focus lands on the "Edit" and "Delete" buttons. (#1822) Co-authored-by: Satyapriya Bai --- src/Common/TableEntity.tsx | 4 ++-- .../GraphExplorerComponent/NodePropertiesComponent.tsx | 4 ++-- .../Graph/NewVertexComponent/NewVertexComponent.tsx | 8 +++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Common/TableEntity.tsx b/src/Common/TableEntity.tsx index f3d0b5244..eece32ecb 100644 --- a/src/Common/TableEntity.tsx +++ b/src/Common/TableEntity.tsx @@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent = ({ editEntity = ({ delete entity - Delete + Delete ); } else { @@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component< aria-label="Edit properties" onActivated={expandClickHandler} > - Edit + Edit )} diff --git a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx index de357989a..6b20cfcb0 100644 --- a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx +++ b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.tsx @@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent = ( className="rightPaneTrashIcon rightPaneBtns" tabIndex={0} role="button" + aria-label={`Delete ${data.key}`} onClick={(event: React.MouseEvent) => removeNewVertexProperty(event, index)} onKeyPress={(event: React.KeyboardEvent) => removeNewVertexPropertyKeyPress(event, index) } > - Remove property + Remove property
From a08415e7bcf9683cee3e37459005972a5693b49a Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Mon, 29 Apr 2024 22:26:27 +0530 Subject: [PATCH 27/67] [3100018:[Programmatic Access - Azure Cosmos DB - Edit Property]: Text Area edit field does not have a label under 'Edit Property' pane. (#1819) Co-authored-by: Satyapriya Bai --- src/Explorer/Panes/Tables/AddTableEntityPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx index 9ad999aa0..7d73ccc1f 100644 --- a/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx +++ b/src/Explorer/Panes/Tables/AddTableEntityPanel.tsx @@ -261,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent = { entityChange(newInput, selectedRow, "value"); From 92246144f710b2dcdc4e9b8613c54344aca7f753 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Mon, 29 Apr 2024 16:25:58 -0400 Subject: [PATCH 28/67] Enable Legacy Mongo Shell in Fairfax (#1829) * enable Mongo Proxy and LMS in sovereign clouds * remove mooncake --------- Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.ts | 1 + src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 907b0305e..d9aa0fb4c 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -677,6 +677,7 @@ export function useMongoProxyEndpoint(api: string): boolean { MongoProxyEndpoints.Local, MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, ]; let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; if ( diff --git a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts index 0c138ff61..8b16816ab 100644 --- a/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts +++ b/src/Explorer/Tabs/MongoShellTab/getMongoShellUrl.test.ts @@ -3,7 +3,6 @@ import { updateUserContext, userContext } from "../../../UserContext"; import { getMongoShellUrl } from "./getMongoShellUrl"; const mongoBackendEndpoint = "https://localhost:1234"; -const hostedExplorerURL = "https://cosmos.azure.com/"; describe("getMongoShellUrl", () => { let queryString = ""; @@ -13,7 +12,6 @@ describe("getMongoShellUrl", () => { updateConfigContext({ BACKEND_ENDPOINT: mongoBackendEndpoint, - hostedExplorerURL: hostedExplorerURL, platform: Platform.Hosted, }); From b023250e67e834ee8485670a1605973d0db88936 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:46:24 -0700 Subject: [PATCH 29/67] First set of changes for Notebooks removal. (#1816) * First set of changes for Notebooks removal. * Fix unit test snapshots. --- src/Contracts/ActionContracts.ts | 5 - .../SettingsComponent.test.tsx.snap | 16 - src/Explorer/Explorer.tsx | 612 +----------------- src/Explorer/Notebook/NotebookManager.tsx | 27 +- src/Explorer/OpenActions/OpenActions.tsx | 12 - .../CopyNotebookPane/CopyNotebookPane.tsx | 154 ----- .../CopyNotebookPaneComponent.tsx | 120 ---- .../GitHubReposPanel.test.tsx.snap | 4 - .../StringInputPane.test.tsx.snap | 4 - .../QueryCopilotTab.test.tsx.snap | 4 - src/Explorer/SplashScreen/SplashScreen.tsx | 28 +- src/Explorer/Tabs/NotebookV2Tab.ts | 336 +--------- src/Explorer/Tree/ResourceTree.tsx | 432 +------------ src/Explorer/Tree/ResourceTreeAdapter.tsx | 497 +------------- src/Utils/GalleryUtils.ts | 2 - 15 files changed, 11 insertions(+), 2242 deletions(-) delete mode 100644 src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx delete mode 100644 src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx diff --git a/src/Contracts/ActionContracts.ts b/src/Contracts/ActionContracts.ts index f8fc956e6..cf4b66ed6 100644 --- a/src/Contracts/ActionContracts.ts +++ b/src/Contracts/ActionContracts.ts @@ -68,10 +68,6 @@ export interface OpenPane extends DataExplorerAction { paneKind: PaneKind | string; } -export interface OpenSampleNotebook extends DataExplorerAction { - path: string; -} - /** * The types of actions that the DataExplorer supports performing upon opening. */ @@ -80,5 +76,4 @@ export enum ActionType { OpenCollectionTab, OpenPane, TransmitCachedData, - OpenSampleNotebook, } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index ab7abac11..b08834dd6 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -29,8 +29,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -47,10 +45,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -107,8 +103,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -125,10 +119,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -224,8 +216,6 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -242,10 +232,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, @@ -271,8 +259,6 @@ exports[`SettingsComponent renders 1`] = ` } explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -289,10 +275,8 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 4af478475..368168a4a 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,4 +1,3 @@ -import { Link } from "@fluentui/react/lib/Link"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { sendMessage } from "Common/MessageHandler"; import { Platform, configContext } from "ConfigContext"; @@ -16,7 +15,7 @@ import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants"; +import { Areas, ConnectionStatusType, HttpStatusCodes, PoolIdType } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; @@ -32,34 +31,23 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants" import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; -import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; -import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; -import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; -import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import * as FileSystemUtil from "./Notebook/FileSystemUtil"; -import { SnapshotRequest } from "./Notebook/NotebookComponent/types"; -import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; +import { NotebookContentItem } from "./Notebook/NotebookContentItem"; import type NotebookManager from "./Notebook/NotebookManager"; -import { NotebookPaneContent } from "./Notebook/NotebookManager"; -import { NotebookUtil } from "./Notebook/NotebookUtil"; import { useNotebook } from "./Notebook/useNotebook"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; -import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; -import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import TabsBase from "./Tabs/TabsBase"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; @@ -87,7 +75,6 @@ export default class Explorer { // Notebooks public notebookManager?: NotebookManager; - private _isInitializingNotebooks: boolean; private notebookToImport: { name: string; content: string; @@ -99,7 +86,6 @@ export default class Explorer { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); - this._isInitializingNotebooks = false; this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); useNotebook.subscribe( @@ -205,12 +191,10 @@ export default class Explorer { container: this, resourceTree: this.resourceTree, refreshCommandBarButtons: () => this.refreshCommandBarButtons(), - refreshNotebookList: () => this.refreshNotebookList(), }); } this.refreshCommandBarButtons(); - this.refreshNotebookList(); } public openEnableSynapseLinkDialog(): void { @@ -373,7 +357,6 @@ export default class Explorer { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); - this.refreshNotebookList(); }; // Facade @@ -381,19 +364,6 @@ export default class Explorer { window.open(Constants.Urls.feedbackEmail, "_blank"); }; - public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - throw new Error("No database account specified"); - } - - if (this._isInitializingNotebooks) { - return; - } - this._isInitializingNotebooks = true; - this.refreshNotebookList(); - this._isInitializingNotebooks = false; - } - public async allocateContainer(poolId: PoolIdType, mode?: string): Promise { const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false; const notebookServerInfo = shouldUseNotebookStates @@ -472,8 +442,6 @@ export default class Explorer { ? useNotebook.getState().setIsAllocating(false) : useQueryCopilot.getState().setIsAllocatingContainer(false); this.refreshCommandBarButtons(); - this.refreshNotebookList(); - this._isInitializingNotebooks = false; } } } @@ -510,104 +478,6 @@ export default class Explorer { .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } - public resetNotebookWorkspace(): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { - handleError( - "Attempt to reset notebook workspace, but notebook is not enabled", - "Explorer/resetNotebookWorkspace", - ); - return; - } - const dialogContent = useNotebook.getState().isPhoenixNotebooks - ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" - : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; - - const resetConfirmationDialogProps: DialogProps = { - isModal: true, - title: "Reset Workspace", - subText: dialogContent, - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._resetNotebookWorkspace, - onSecondaryButtonClick: () => useDialog.getState().closeDialog(), - }; - useDialog.getState().openDialog(resetConfirmationDialogProps); - } - - private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - return false; - } - try { - const { value: workspaces } = await listByDatabaseAccount( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - ); - return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); - return false; - } - } - - private _resetNotebookWorkspace = async () => { - useDialog.getState().closeDialog(); - const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); - let connectionStatus: ContainerConnectionInfo; - try { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { - const error = "No server endpoint detected"; - Logger.logError(error, "NotebookContainerClient/resetWorkspace"); - logConsoleError(error); - return; - } - TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - if (useNotebook.getState().isPhoenixNotebooks) { - useTabs.getState().closeAllNotebookTabs(true); - connectionStatus = { - status: ConnectionStatusType.Connecting, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); - } - const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); - if (connectionInfo?.status !== HttpStatusCodes.OK) { - throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); - } - if (!connectionInfo?.data?.phoenixServiceUrl) { - throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.setNotebookInfo(true, connectionInfo, connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - logConsoleInfo("Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - } catch (error) { - logConsoleError(`Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }); - if (useNotebook.getState().isPhoenixNotebooks) { - connectionStatus = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - throw error; - } finally { - clearInProgressMessage(); - } - }; - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -696,406 +566,6 @@ export default class Explorer { } } - public uploadFile( - name: string, - content: string, - parent: NotebookContentItem, - isGithubTree?: boolean, - ): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to upload notebook, but notebook is not enabled"; - handleError(error, "Explorer/uploadFile"); - throw new Error(error); - } - - const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); - promise - .then(() => this.resourceTree.triggerRender()) - .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); - return promise; - } - - public async importAndOpen(path: string): Promise { - const name = NotebookUtil.getName(path); - const item = NotebookUtil.createNotebookContentItem(name, path, "file"); - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { - const existingItem = _.find(parent.children, (node) => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const content = await this.readFile(item); - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - return Promise.resolve(false); - } - - public async importAndOpenContent(name: string, content: string): Promise { - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { - if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { - this.notebookToImport = undefined; // we don't want to try opening this notebook again - } - - const existingItem = _.find(parent.children, (node) => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - this.notebookToImport = { name, content }; // we'll try opening this notebook later on - return Promise.resolve(false); - } - - public async publishNotebook( - name: string, - content: NotebookPaneContent, - notebookContentRef?: string, - onTakeSnapshot?: (request: SnapshotRequest) => void, - onClosePanel?: () => void, - ): Promise { - if (this.notebookManager) { - await this.notebookManager.openPublishNotebookPane( - name, - content, - notebookContentRef, - onTakeSnapshot, - onClosePanel, - ); - } - } - - public copyNotebook(name: string, content: string): void { - this.notebookManager?.openCopyNotebookPane(name, content); - } - - /** - * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. - * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. - * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder - * will not fetch its content if the children array exists (and has only one child which was manually created). - * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. - * - * @param name - * @param path - */ - public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { - return NotebookUtil.createNotebookContentItem(name, path, "file"); - } - - public async openNotebook(notebookContentItem: NotebookContentItem): Promise { - if (!notebookContentItem || !notebookContentItem.path) { - throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); - } - if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - } - - const notebookTabs = useTabs - .getState() - .getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab) => - (tab as NotebookV2Tab).notebookPath && - FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path), - ) as NotebookV2Tab[]; - let notebookTab = notebookTabs && notebookTabs[0]; - - if (notebookTab) { - useTabs.getState().activateTab(notebookTab); - } else { - const options: NotebookTabOptions = { - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.NotebookV2, - node: undefined, - title: notebookContentItem.name, - tabPath: notebookContentItem.path, - collection: undefined, - masterKey: userContext.masterKey || "", - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: undefined, - container: this, - notebookContentItem, - }; - - try { - const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); - notebookTab = new NotebookTabV2.default(options); - useTabs.getState().activateNewTab(notebookTab); - } catch (reason) { - console.error("Import NotebookV2Tab failed!", reason); - return false; - } - } - - return true; - } - - public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to rename notebook, but notebook is not enabled"; - handleError(error, "Explorer/renameNotebook"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = useTabs - .getState() - .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); - }); - if (openedNotebookTabs.length > 0) { - useDialog - .getState() - .showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); - } else { - useSidePanel.getState().openSidePanel( - "Rename Notebook", - { - useSidePanel.getState().closeSidePanel(); - this.resourceTree.triggerRender(); - }} - inputLabel="Enter new notebook name" - submitButtonLabel="Rename" - errorMessage="Could not rename notebook" - inProgressMessage="Renaming notebook to" - successMessage="Renamed notebook to" - paneTitle="Rename Notebook" - defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} - onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) - } - notebookFile={notebookFile} - />, - ); - } - } - - public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create notebook directory, but notebook is not enabled"; - handleError(error, "Explorer/onCreateDirectory"); - throw new Error(error); - } - - useSidePanel.getState().openSidePanel( - "Create new directory", - { - useSidePanel.getState().closeSidePanel(); - this.resourceTree.triggerRender(); - }} - errorMessage="Could not create directory " - inProgressMessage="Creating directory " - successMessage="Created directory " - inputLabel="Enter new directory name" - paneTitle="Create new directory" - submitButtonLabel="Create" - defaultInput="" - onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => - this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) - } - notebookFile={parent} - />, - ); - } - - public readFile(notebookFile: NotebookContentItem): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to read file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); - } - - public downloadFile(notebookFile: NotebookContentItem): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to download file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( - (content: string) => { - const blob = stringToBlob(content, "text/plain"); - if (navigator.msSaveBlob) { - // for IE and Edge - navigator.msSaveBlob(blob, notebookFile.name); - } else { - const downloadLink: HTMLAnchorElement = document.createElement("a"); - const url = URL.createObjectURL(blob); - downloadLink.href = url; - downloadLink.target = "_self"; - downloadLink.download = notebookFile.name; - - // for some reason, FF displays the download prompt only when - // the link is added to the dom so we add and remove it - document.body.appendChild(downloadLink); - downloadLink.click(); - downloadLink.remove(); - } - - clearMessage(); - }, - (error) => { - logConsoleError(`Could not download notebook ${getErrorMessage(error)}`); - clearMessage(); - }, - ); - } - - private refreshNotebookList = async (): Promise => { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - return; - } - - await this.resourceTree.initialize(); - await useNotebook.getState().initializeNotebooksTree(this.notebookManager); - - this.notebookManager?.refreshPinnedRepos(); - if (this.notebookToImport) { - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - }; - - public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to delete notebook file, but notebook is not enabled"; - handleError(error, "Explorer/deleteNotebookFile"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = useTabs - .getState() - .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); - }); - if (openedNotebookTabs.length > 0) { - useDialog - .getState() - .showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); - return Promise.reject(); - } - - if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { - useDialog.getState().openDialog({ - isModal: true, - title: "Unable to delete file", - subText: "Directory is not empty.", - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: () => useDialog.getState().closeDialog(), - onSecondaryButtonClick: undefined, - }); - return Promise.reject(); - } - - return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( - () => logConsoleInfo(`Successfully deleted: ${item.path}`), - (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`), - ); - } - - /** - * This creates a new notebook file, then opens the notebook - */ - public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create new notebook, but notebook is not enabled"; - handleError(error, "Explorer/onNewNotebookClicked"); - throw new Error(error); - } - if (useNotebook.getState().isPhoenixNotebooks) { - if (isGithubTree) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } else { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookModalTitle, - undefined, - "Create", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } - } - - private getNewNoteWarningText(): JSX.Element { - return ( - <> -

{Notebook.newNotebookModalContent1}

-
-

- {Notebook.newNotebookModalContent2} - - {Notebook.learnMore} - -

- - ); - } - - private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { - const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { - dataExplorerArea: Constants.Areas.Notebook, - }); - - this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent, isGithubTree) - .then((newFile: NotebookContentItem) => { - logConsoleInfo(`Successfully created: ${newFile.name}`); - TelemetryProcessor.traceSuccess( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - }, - startKey, - ); - return this.openNotebook(newFile); - }) - .then(() => this.resourceTree.triggerRender()) - .catch((error) => { - const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; - logConsoleError(errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }) - .finally(clearInProgressMessage); - } - // TODO: Delete this function when ResourceTreeAdapter is removed. public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { @@ -1252,32 +722,6 @@ export default class Explorer { } } - public async handleOpenFileAction(path: string): Promise { - if (useNotebook.getState().isPhoenixNotebooks === undefined) { - await useNotebook.getState().getPhoenixStatus(); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - } - - // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb - // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly - // calling GitHub. For now convert this url to a raw url and download content. - const gitHubInfo = fromContentUri(path); - if (gitHubInfo) { - const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); - const response = await fetch(rawUrl); - if (response.status === Constants.HttpStatusCodes.OK) { - this.notebookToImport = { - name: NotebookUtil.getName(path), - content: await response.text(), - }; - - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - } - } - public openUploadItemsPanePane(): void { useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } @@ -1287,54 +731,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public openUploadFilePanel(parent?: NotebookContentItem): void { - if (useNotebook.getState().isPhoenixNotebooks) { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookUploadModalTitle, - undefined, - "Upload", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - } - } - - private uploadFilePanel(parent?: NotebookContentItem): void { - useSidePanel - .getState() - .openSidePanel( - "Upload file to notebook server", - this.uploadFile(name, content, parent)} />, - ); - } - - public getDownloadModalConent(fileName: string): JSX.Element { - if (useNotebook.getState().isPhoenixNotebooks) { - return ( - <> -

{Notebook.galleryNotebookDownloadContent1}

-
-

- {Notebook.galleryNotebookDownloadContent2} - - {Notebook.learnMore} - -

- - ); - } - return

Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook.

; - } - public async refreshExplorer(): Promise { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { userContext.authType === AuthType.ResourceToken @@ -1359,10 +755,6 @@ export default class Explorer { dataExplorerArea: Constants.Areas.Notebook, }); - if (useNotebook.getState().isPhoenixNotebooks) { - await this.initNotebooks(userContext.databaseAccount); - } - await this.refreshSampleData(); } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 3ccbefcaf..45afe6061 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -12,15 +12,13 @@ import * as Logger from "../../Common/Logger"; import { GitHubClient } from "../../GitHub/GitHubClient"; import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { JunoClient } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; import { getFullName } from "../../Utils/UserUtils"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; -import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; @@ -40,7 +38,6 @@ export interface NotebookManagerOptions { container: Explorer; resourceTree: ResourceTreeAdapter; refreshCommandBarButtons: () => void; - refreshNotebookList: () => void; } export default class NotebookManager { @@ -81,10 +78,6 @@ export default class NotebookManager { contents.JupyterContentProvider, ); - this.notebookClient = new NotebookContainerClient(() => - this.params.container.initNotebooks(userContext?.databaseAccount), - ); - this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider); this.gitHubOAuthService.getTokenObservable().subscribe((token) => { @@ -106,11 +99,9 @@ export default class NotebookManager { } this.params.refreshCommandBarButtons(); - this.params.refreshNotebookList(); }); this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { - this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.triggerRender(); useNotebook.getState().initializeGitHubRepos(pinnedRepos); }); @@ -149,22 +140,6 @@ export default class NotebookManager { ); } - public openCopyNotebookPane(name: string, content: string): void { - const { container } = this.params; - useSidePanel - .getState() - .openSidePanel( - "Copy Notebook", - , - ); - } - // Octokit's error handler uses any // eslint-disable-next-line @typescript-eslint/no-explicit-any private onGitHubClientError = (error: any): void => { diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index f3ef288c8..e2059cdba 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -195,17 +195,5 @@ export function handleOpenAction( return true; } - if ( - action.actionType === ActionContracts.ActionType.OpenSampleNotebook || - action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook] - ) { - openFile(action as ActionContracts.OpenSampleNotebook, explorer); - return true; - } - return false; } - -function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) { - explorer.handleOpenFileAction(decodeURIComponent(action.path)); -} diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx deleted file mode 100644 index 0f7927b3b..000000000 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { IDropdownOption } from "@fluentui/react"; -import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; -import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants"; -import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; -import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; -import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; -import { useSidePanel } from "../../../hooks/useSidePanel"; -import Explorer from "../../Explorer"; -import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; -import { useNotebook } from "../../Notebook/useNotebook"; -import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; -import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; - -interface Location { - type: "MyNotebooks" | "GitHub"; - - // GitHub - owner?: string; - repo?: string; - branch?: string; -} -export interface CopyNotebookPanelProps { - name: string; - content: string; - container: Explorer; - junoClient: JunoClient; - gitHubOAuthService: GitHubOAuthService; -} - -export const CopyNotebookPane: FunctionComponent = ({ - name, - content, - container, - junoClient, - gitHubOAuthService, -}: CopyNotebookPanelProps) => { - const closeSidePanel = useSidePanel((state) => state.closeSidePanel); - const [isExecuting, setIsExecuting] = useState(); - const [formError, setFormError] = useState(""); - const [pinnedRepos, setPinnedRepos] = useState(); - const [selectedLocation, setSelectedLocation] = useState(); - - useEffect(() => { - open(); - }, []); - - const open = async (): Promise => { - if (gitHubOAuthService.isLoggedIn()) { - const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit"); - } - - if (response.data?.length > 0) { - setPinnedRepos(response.data); - } - } - }; - - const submit = async (): Promise => { - let destination: string = selectedLocation?.type; - let clearMessage: () => void; - setIsExecuting(true); - - try { - if (!selectedLocation) { - throw new Error(`No location selected`); - } - - if (selectedLocation.type === "GitHub") { - destination = `${destination} - ${GitHubUtils.toRepoFullName( - selectedLocation.owner, - selectedLocation.repo, - )} - ${selectedLocation.branch}`; - } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) { - destination = useNotebook.getState().notebookFolderName; - } - - clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); - - const notebookContentItem = await copyNotebook(selectedLocation); - if (!notebookContentItem) { - throw new Error(`Failed to upload ${name}`); - } - - NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`); - closeSidePanel(); - } catch (error) { - const errorMessage = getErrorMessage(error); - setFormError(`Failed to copy ${name} to ${destination}`); - handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); - } finally { - clearMessage && clearMessage(); - setIsExecuting(false); - } - }; - - const copyNotebook = async (location: Location): Promise => { - let parent: NotebookContentItem; - let isGithubTree: boolean; - switch (location.type) { - case "MyNotebooks": - parent = { - name: useNotebook.getState().notebookFolderName, - path: useNotebook.getState().notebookBasePath, - type: NotebookContentItemType.Directory, - }; - isGithubTree = false; - if (useNotebook.getState().isPhoenixNotebooks) { - await container.allocateContainer(PoolIdType.DefaultPoolId); - } - break; - - case "GitHub": - parent = { - name: selectedLocation.branch, - path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), - type: NotebookContentItemType.Directory, - }; - isGithubTree = true; - break; - - default: - throw new Error(`Unsupported location type ${location.type}`); - } - - return container.uploadFile(name, content, parent, isGithubTree); - }; - - const onDropDownChange = (_: FormEvent, option?: IDropdownOption): void => { - setSelectedLocation(option?.data); - }; - - const props: RightPaneFormProps = { - formError, - isExecuting: isExecuting, - submitButtonText: "OK", - onSubmit: () => submit(), - }; - - const copyNotebookPaneProps: CopyNotebookPaneProps = { - name, - pinnedRepos, - onDropDownChange: onDropDownChange, - }; - - return ( - - - - ); -}; diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx deleted file mode 100644 index 5cd0cfdc1..000000000 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { - Dropdown, - IDropdownOption, - IDropdownProps, - IRenderFunction, - ISelectableOption, - Label, - SelectableOptionMenuItemType, - Stack, - Text, -} from "@fluentui/react"; -import { GitHubReposTitle } from "Explorer/Tree/ResourceTree"; -import React, { FormEvent, FunctionComponent } from "react"; -import { IPinnedRepo } from "../../../Juno/JunoClient"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import { useNotebook } from "../../Notebook/useNotebook"; - -interface Location { - type: "MyNotebooks" | "GitHub"; - - // GitHub - owner?: string; - repo?: string; - branch?: string; -} - -export interface CopyNotebookPaneProps { - name: string; - pinnedRepos: IPinnedRepo[]; - onDropDownChange: (_: FormEvent, option?: IDropdownOption) => void; -} - -export const CopyNotebookPaneComponent: FunctionComponent = ({ - name, - pinnedRepos, - onDropDownChange, -}: CopyNotebookPaneProps) => { - const BranchNameWhiteSpace = " "; - - const onRenderDropDownTitle: IRenderFunction = (options: IDropdownOption[]): JSX.Element => { - return {options.length && options[0].title}; - }; - - const onRenderDropDownOption: IRenderFunction = (option: ISelectableOption): JSX.Element => { - return {option.text}; - }; - - const getDropDownOptions = (): IDropdownOption[] => { - const options: IDropdownOption[] = []; - options.push({ - key: "MyNotebooks-Item", - text: useNotebook.getState().notebookFolderName, - title: useNotebook.getState().notebookFolderName, - data: { - type: "MyNotebooks", - } as Location, - }); - - if (pinnedRepos && pinnedRepos.length > 0) { - options.push({ - key: "GitHub-Header-Divider", - text: undefined, - itemType: SelectableOptionMenuItemType.Divider, - }); - - options.push({ - key: "GitHub-Header", - text: GitHubReposTitle, - itemType: SelectableOptionMenuItemType.Header, - }); - - pinnedRepos.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - options.push({ - key: `GitHub-Repo-${repoFullName}`, - text: repoFullName, - disabled: true, - }); - - pinnedRepo.branches.forEach((branch) => - options.push({ - key: `GitHub-Repo-${repoFullName}-${branch.name}`, - text: `${BranchNameWhiteSpace}${branch.name}`, - title: `${repoFullName} - ${branch.name}`, - data: { - type: "GitHub", - owner: pinnedRepo.owner, - repo: pinnedRepo.name, - branch: branch.name, - } as Location, - }), - ); - }); - } - - return options; - }; - const dropDownProps: IDropdownProps = { - label: "Location", - ariaLabel: "Location", - placeholder: "Select an option", - onRenderTitle: onRenderDropDownTitle, - onRenderOption: onRenderDropDownOption, - options: getDropDownOptions(), - onChange: onDropDownChange, - }; - - return ( -
- - - - {name} - - - - -
- ); -}; diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 4a3a8942e..aa2137c42 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -17,8 +17,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` addRepoProps={ Object { "container": Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -35,10 +33,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, }, diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 8054abe19..566808f4b 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -7,8 +7,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` errorMessage="Could not create directory " explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -25,10 +23,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 26b52ff90..6d875fe2e 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -22,8 +22,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = databaseId="CopilotSampleDb" explorer={ Explorer { - "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -40,10 +38,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = "queriesClient": QueriesClient { "container": [Circular], }, - "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], - "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index f4ebb9cd0..e9477e088 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -25,11 +25,9 @@ import * as React from "react"; import ConnectIcon from "../../../images/Connect_color.svg"; import ContainersIcon from "../../../images/Containers.svg"; import LinkIcon from "../../../images/Link_blue.svg"; -import NotebookColorIcon from "../../../images/Notebooks.svg"; import PowerShellIcon from "../../../images/PowerShell.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import * as Constants from "../../Common/Constants"; import { userContext } from "../../UserContext"; @@ -410,14 +408,6 @@ export class SplashScreen extends React.Component { }, }; heroes.push(launchQuickstartBtn); - } else if (useNotebook.getState().isPhoenixNotebooks) { - const newNotebookBtn = { - iconSrc: NotebookColorIcon, - title: "New notebook", - description: "Visualize your data stored in Azure Cosmos DB", - onClick: () => this.container.onNewNotebookClicked(), - }; - heroes.push(newNotebookBtn); } heroes.push(this.getShellCard()); @@ -493,28 +483,12 @@ export class SplashScreen extends React.Component { }; } - private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) { - return { - info: path, - iconSrc: NotebookIcon, - title: name, - description: "Notebook", - onClick: () => { - const notebookItem = this.container.createNotebookContentItemFile(name, path); - notebookItem && this.container.openNotebook(notebookItem); - }, - }; - } - private createRecentItems(): SplashScreenItem[] { return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => { switch (activity.type) { default: { - const unknownActivity: never = activity; - throw new Error(`Unknown activity: ${unknownActivity}`); + throw new Error(`Unknown activity: ${activity}`); } - case MostRecentActivity.Type.OpenNotebook: - return this.decorateOpenNotebookActivity(activity); case MostRecentActivity.Type.OpenCollection: return this.decorateOpenCollectionActivity(activity); diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 92e0e6958..fadb43258 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -1,31 +1,11 @@ -import { stringifyNotebook, toJS } from "@nteract/commutable"; import * as ko from "knockout"; import * as Q from "q"; -import { userContext } from "UserContext"; -import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import CutIcon from "../../../images/notebook/Notebook-cut.svg"; -import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg"; -import PasteIcon from "../../../images/notebook/Notebook-paste.svg"; -import RestartIcon from "../../../images/notebook/Notebook-restart.svg"; -import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg"; -import RunIcon from "../../../images/notebook/Notebook-run.svg"; -import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg"; -import SaveIcon from "../../../images/save-cosmos.svg"; -import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { useDialog } from "../Controls/Dialog"; -import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; -import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; -import * as CdbActions from "../Notebook/NotebookComponent/actions"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; -import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { NotebookContentItem } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; @@ -90,275 +70,7 @@ export default class NotebookTabV2 extends NotebookTabBase { } protected getTabsButtons(): CommandButtonComponentProps[] { - const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); - const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted(); - - const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined; - - const saveLabel = "Save"; - const copyToLabel = "Copy to ..."; - const publishLabel = "Publish to gallery"; - const kernelLabel = "No Kernel"; - const runLabel = "Run"; - const runActiveCellLabel = "Run Active Cell"; - const runAllLabel = "Run All"; - const interruptKernelLabel = "Interrupt Kernel"; - const killKernelLabel = "Halt Kernel"; - const restartKernelLabel = "Restart Kernel"; - const clearLabel = "Clear outputs"; - const newCellLabel = "New Cell"; - const cellTypeLabel = "Cell Type"; - const codeLabel = "Code"; - const markdownLabel = "Markdown"; - const rawLabel = "Raw"; - const copyLabel = "Copy"; - const cutLabel = "Cut"; - const pasteLabel = "Paste"; - const cellCodeType = "code"; - const cellMarkdownType = "markdown"; - const cellRawType = "raw"; - - const saveButtonChildren = []; - if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - saveButtonChildren.push({ - iconName: copyToLabel, - onCommandClick: () => this.copyNotebook(), - commandButtonLabel: copyToLabel, - hasPopup: false, - disabled: false, - ariaLabel: copyToLabel, - }); - } - - if (userContext.features.publicGallery) { - saveButtonChildren.push({ - iconName: "PublishContent", - onCommandClick: async () => await this.publishToGallery(), - commandButtonLabel: publishLabel, - hasPopup: false, - disabled: false, - ariaLabel: publishLabel, - }); - } - - let buttons: CommandButtonComponentProps[] = [ - { - iconSrc: SaveIcon, - iconAlt: saveLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookSave(), - commandButtonLabel: saveLabel, - hasPopup: false, - disabled: false, - ariaLabel: saveLabel, - children: saveButtonChildren.length && [ - { - iconName: "Save", - onCommandClick: () => this.notebookComponentAdapter.notebookSave(), - commandButtonLabel: saveLabel, - hasPopup: false, - disabled: false, - ariaLabel: saveLabel, - }, - ...saveButtonChildren, - ], - }, - { - iconSrc: null, - iconAlt: kernelLabel, - onCommandClick: () => {}, - commandButtonLabel: null, - hasPopup: false, - disabled: availableKernels.length < 1, - isDropdown: true, - dropdownPlaceholder: kernelLabel, - dropdownSelectedKey: this.notebookComponentAdapter.getSelectedKernelName(), //this.currentKernelName, - dropdownWidth: 100, - children: availableKernels.map( - (kernel: KernelSpecsDisplay) => - ({ - iconSrc: null, - iconAlt: kernel.displayName, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeKernel(kernel.name), - commandButtonLabel: kernel.displayName, - dropdownItemKey: kernel.name, - hasPopup: false, - disabled: false, - ariaLabel: kernel.displayName, - }) as CommandButtonComponentProps, - ), - ariaLabel: kernelLabel, - }, - { - iconSrc: RunIcon, - iconAlt: runLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAndAdvance(); - this.traceTelemetry(Action.ExecuteCell); - }, - commandButtonLabel: runLabel, - tooltipText: runBtnTooltip, - ariaLabel: runLabel, - hasPopup: false, - disabled: isNotebookUntrusted, - children: [ - { - iconSrc: RunIcon, - iconAlt: runActiveCellLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAndAdvance(); - this.traceTelemetry(Action.ExecuteCell); - }, - commandButtonLabel: runActiveCellLabel, - hasPopup: false, - disabled: false, - ariaLabel: runActiveCellLabel, - }, - { - iconSrc: RunAllIcon, - iconAlt: runAllLabel, - onCommandClick: () => { - this.notebookComponentAdapter.notebookRunAll(); - this.traceTelemetry(Action.ExecuteAllCells); - }, - commandButtonLabel: runAllLabel, - hasPopup: false, - disabled: false, - ariaLabel: runAllLabel, - }, - { - iconSrc: InterruptKernelIcon, - iconAlt: interruptKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookInterruptKernel(), - commandButtonLabel: interruptKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: interruptKernelLabel, - }, - { - iconSrc: KillKernelIcon, - iconAlt: killKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookKillKernel(), - commandButtonLabel: killKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: killKernelLabel, - }, - { - iconSrc: RestartIcon, - iconAlt: restartKernelLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookRestartKernel(), - commandButtonLabel: restartKernelLabel, - hasPopup: false, - disabled: false, - ariaLabel: restartKernelLabel, - }, - ], - }, - { - iconSrc: ClearAllOutputsIcon, - iconAlt: clearLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookClearAllOutputs(), - commandButtonLabel: clearLabel, - hasPopup: false, - disabled: false, - ariaLabel: clearLabel, - }, - { - iconSrc: NewCellIcon, - iconAlt: newCellLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookInsertBelow(), - commandButtonLabel: newCellLabel, - ariaLabel: newCellLabel, - hasPopup: false, - disabled: false, - }, - CommandBarComponentButtonFactory.createDivider(), - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => {}, - commandButtonLabel: null, - ariaLabel: cellTypeLabel, - hasPopup: false, - disabled: false, - isDropdown: true, - dropdownPlaceholder: cellTypeLabel, - dropdownSelectedKey: this.notebookComponentAdapter.getActiveCellTypeStr(), - dropdownWidth: 110, - children: [ - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellCodeType), - commandButtonLabel: codeLabel, - ariaLabel: codeLabel, - dropdownItemKey: cellCodeType, - hasPopup: false, - disabled: false, - }, - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellMarkdownType), - commandButtonLabel: markdownLabel, - ariaLabel: markdownLabel, - dropdownItemKey: cellMarkdownType, - hasPopup: false, - disabled: false, - }, - { - iconSrc: null, - iconAlt: null, - onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellRawType), - commandButtonLabel: rawLabel, - ariaLabel: rawLabel, - dropdownItemKey: cellRawType, - hasPopup: false, - disabled: false, - }, - ], - }, - { - iconSrc: CopyIcon, - iconAlt: copyLabel, - onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), - commandButtonLabel: copyLabel, - ariaLabel: copyLabel, - hasPopup: false, - disabled: false, - children: [ - { - iconSrc: CopyIcon, - iconAlt: copyLabel, - onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), - commandButtonLabel: copyLabel, - ariaLabel: copyLabel, - hasPopup: false, - disabled: false, - }, - { - iconSrc: CutIcon, - iconAlt: cutLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookCut(), - commandButtonLabel: cutLabel, - ariaLabel: cutLabel, - hasPopup: false, - disabled: false, - }, - { - iconSrc: PasteIcon, - iconAlt: pasteLabel, - onCommandClick: () => this.notebookComponentAdapter.notebookPaste(), - commandButtonLabel: pasteLabel, - ariaLabel: pasteLabel, - hasPopup: false, - disabled: false, - }, - ], - }, - // TODO: Uncomment when undo/redo is reimplemented in nteract - ]; - return buttons; + return []; } protected buildCommandBarOptions(): void { @@ -382,50 +94,4 @@ export default class NotebookTabV2 extends NotebookTabBase { sparkClusterConnectionInfo, ); } - - private publishToGallery = async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.CommandBarMenu, - }); - - const notebookReduxStore = NotebookTabV2.clientManager.getStore(); - const unsubscribe = notebookReduxStore.subscribe(() => { - const cdbState = (notebookReduxStore.getState() as CdbAppState).cdb; - useNotebookSnapshotStore.setState({ - snapshot: cdbState.notebookSnapshot?.imageSrc, - error: cdbState.notebookSnapshotError, - }); - }); - - const notebookContent = this.notebookComponentAdapter.getContent(); - const notebookContentRef = this.notebookComponentAdapter.contentRef; - const onPanelClose = (): void => { - unsubscribe(); - useNotebookSnapshotStore.setState({ - snapshot: undefined, - error: undefined, - }); - notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(undefined)); - }; - - await this.container.publishNotebook( - notebookContent.name, - notebookContent.content, - notebookContentRef, - (request: SnapshotRequest) => notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(request)), - onPanelClose, - ); - }; - - private copyNotebook = () => { - const notebookContent = this.notebookComponentAdapter.getContent(); - let content: string; - if (typeof notebookContent.content === "string") { - content = notebookContent.content; - } else { - content = stringifyNotebook(toJS(notebookContent.content)); - } - - this.container.copyNotebook(notebookContent.name, content); - }; } diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b5f759534..0933eac7b 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,42 +1,23 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; -import DeleteIcon from "../../../images/delete.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; -import FileIcon from "../../../images/notebook/file-cosmos.svg"; -import PublishIcon from "../../../images/notebook/publish_content.svg"; -import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; -import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { useDialog } from "../Controls/Dialog"; -import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; -import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -45,391 +26,21 @@ import StoredProcedure from "./StoredProcedure"; import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; -export const MyNotebooksTitle = "My Notebooks"; -export const GitHubReposTitle = "GitHub repos"; - interface ResourceTreeProps { container: Explorer; } export const ResourceTree: React.FC = ({ container }: ResourceTreeProps): JSX.Element => { const databases = useDatabases((state) => state.databases); - const { - isNotebookEnabled, - myNotebooksContentRoot, - galleryContentRoot, - gitHubNotebooksContentRoot, - updateNotebookItem, - } = useNotebook( + const { isNotebookEnabled } = useNotebook( (state) => ({ isNotebookEnabled: state.isNotebookEnabled, - myNotebooksContentRoot: state.myNotebooksContentRoot, - galleryContentRoot: state.galleryContentRoot, - gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot, - updateNotebookItem: state.updateNotebookItem, }), shallow, ); - const { activeTab, refreshActiveTab } = useTabs(); + const { refreshActiveTab } = useTabs(); const showScriptNodes = configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); - const pseudoDirPath = "PsuedoDir"; - - const buildGalleryCallout = (): JSX.Element => { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - container.openGallery(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - }; - - const buildNotebooksTree = (): TreeNode => { - const notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (!useNotebook.getState().isPhoenixNotebooks) { - notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); - } else { - if (galleryContentRoot) { - notebooksTree.children.push(buildGalleryNotebooksTree()); - } - - if ( - myNotebooksContentRoot && - useNotebook.getState().isPhoenixNotebooks && - useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected - ) { - notebooksTree.children.push(buildMyNotebooksTree()); - } - if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(buildGitHubNotebooksTree(true)); - } - } - return notebooksTree; - }; - - const buildNotebooksTemporarilyDownTree = (): TreeNode => { - return { - label: Notebook.temporarilyDownMsg, - className: "clickDisabled", - }; - }; - - const buildGalleryNotebooksTree = (): TreeNode => { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => container.openGallery(), - isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, - }; - }; - - const buildMyNotebooksTree = (): TreeNode => { - const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( - myNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - }; - - const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { - const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( - gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - true, - ); - const manageGitContextMenu: TreeNodeMenuItem[] = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - gitHubNotebooksTree.contextMenu = manageGitContextMenu; - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - }; - - const buildChildNodes = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode[] => { - if (!item || !item.children) { - return []; - } else { - return item.children.map((item) => { - const result = - item.type === NotebookContentItemType.Directory - ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) - : buildNotebookFileNode(item, onFileClick, isGithubTree); - result.timestamp = item.timestamp; - return result; - }); - } - }; - - const buildNotebookFileNode = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode => { - return { - label: item.name, - iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, - className: "notebookHeader", - onClick: () => onFileClick(item), - isSelected: () => { - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: createFileContextMenu(container, item, isGithubTree), - data: item, - }; - }; - - const createFileContextMenu = ( - container: Explorer, - item: NotebookContentItem, - isGithubTree?: boolean, - ): TreeNodeMenuItem[] => { - let items: TreeNodeMenuItem[] = [ - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item, isGithubTree), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => container.deleteNotebookFile(item, isGithubTree), - "Cancel", - undefined, - ); - }, - }, - { - label: "Copy to ...", - iconSrc: CopyIcon, - onClick: () => copyNotebook(container, item), - }, - { - label: "Download", - iconSrc: NotebookIcon, - onClick: () => container.downloadFile(item), - }, - ]; - - if (item.type === NotebookContentItemType.Notebook && userContext.features.publicGallery) { - items.push({ - label: "Publish to gallery", - iconSrc: PublishIcon, - onClick: async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.ResourceTreeMenu, - }); - - const content = await container.readFile(item); - if (content) { - await container.publishNotebook(item.name, content); - } - }, - }); - } - - // "Copy to ..." isn't needed if github locations are not available - if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - items = items.filter((item) => item.label !== "Copy to ..."); - } - - return items; - }; - - const copyNotebook = async (container: Explorer, item: NotebookContentItem) => { - const content = await container.readFile(item); - if (content) { - container.copyNotebook(item.name, content); - } - }; - - const createDirectoryContextMenu = ( - container: Explorer, - item: NotebookContentItem, - isGithubTree?: boolean, - ): TreeNodeMenuItem[] => { - let items: TreeNodeMenuItem[] = [ - { - label: "Refresh", - iconSrc: RefreshIcon, - onClick: () => loadSubitems(item, isGithubTree), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => container.deleteNotebookFile(item, isGithubTree), - "Cancel", - undefined, - ); - }, - }, - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => container.renameNotebook(item, isGithubTree), - }, - { - label: "New Directory", - iconSrc: NewNotebookIcon, - onClick: () => container.onCreateDirectory(item, isGithubTree), - }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => container.openUploadFilePanel(item), - }, - ]; - - //disallow renaming of temporary notebook workspace - if (item?.path === useNotebook.getState().notebookBasePath) { - items = items.filter((item) => item.label !== "Rename"); - } - - // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" - if (GitHubUtils.fromContentUri(item.path)) { - items = items.filter( - (item) => - item.label !== "Delete" && - item.label !== "Rename" && - item.label !== "New Directory" && - item.label !== "Upload File", - ); - } - - return items; - }; - - const buildNotebookDirectoryNode = ( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - isGithubTree?: boolean, - ): TreeNode => { - return { - label: item.name, - iconSrc: undefined, - className: "notebookHeader", - isAlphaSorted: true, - isLeavesParentsSeparate: true, - onClick: () => { - if (!item.children) { - loadSubitems(item, isGithubTree); - } - }, - isSelected: () => { - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, - data: item, - children: buildChildNodes(item, onFileClick, isGithubTree), - }; - }; const buildDataTree = (): TreeNode => { const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => { @@ -757,11 +368,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc return traverse(schema); }; - const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise => { - const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); - updateNotebookItem(updatedItem, isGithubTree); - }; - const dataRootNode = buildDataTree(); const isSampleDataEnabled = useQueryCopilot().copilotEnabled && @@ -775,46 +381,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc {!isNotebookEnabled && !isSampleDataEnabled && ( )} - {isNotebookEnabled && !isSampleDataEnabled && ( - <> - - - - - - - {/* {buildGalleryCallout()} */} - - )} {!isNotebookEnabled && isSampleDataEnabled && ( <> - + - - {/* {buildGalleryCallout()} */} - - )} - {isNotebookEnabled && isSampleDataEnabled && ( - <> - - - - - - - - - - - - - {/* {buildGalleryCallout()} */} )} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index b22f0f916..67fb54773 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,42 +1,21 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; -import DeleteIcon from "../../../images/delete.svg"; -import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; -import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; -import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; -import FileIcon from "../../../images/notebook/file-cosmos.svg"; -import PublishIcon from "../../../images/notebook/publish_content.svg"; -import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { IPinnedRepo } from "../../Juno/JunoClient"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; -import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; -import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { useDialog } from "../Controls/Dialog"; -import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; +import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; -import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -46,19 +25,8 @@ import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; export class ResourceTreeAdapter implements ReactAdapter { - public static readonly MyNotebooksTitle = "My Notebooks"; - public static readonly GitHubReposTitle = "GitHub repos"; - - private static readonly DataTitle = "DATA"; - private static readonly NotebooksTitle = "NOTEBOOKS"; - private static readonly PseudoDirPath = "PsuedoDir"; - public parameters: ko.Observable; - public galleryContentRoot: NotebookContentItem; - public myNotebooksContentRoot: NotebookContentItem; - public gitHubNotebooksContentRoot: NotebookContentItem; - public constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); @@ -76,111 +44,9 @@ export class ResourceTreeAdapter implements ReactAdapter { this.triggerRender(); } - private traceMyNotebookTreeInfo() { - const myNotebooksTree = this.myNotebooksContentRoot; - if (myNotebooksTree.children) { - // Count 1st generation children (tree is lazy-loaded) - const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; - myNotebooksTree.children.forEach((treeNode) => { - switch ((treeNode as NotebookContentItem).type) { - case NotebookContentItemType.File: - nodeCounts.files++; - break; - case NotebookContentItemType.Directory: - nodeCounts.directories++; - break; - case NotebookContentItemType.Notebook: - nodeCounts.notebooks++; - break; - default: - break; - } - }); - TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts }); - } - } - public renderComponent(): JSX.Element { const dataRootNode = this.buildDataTree(); - const notebooksRootNode = this.buildNotebooksTrees(); - - if (useNotebook.getState().isNotebookEnabled) { - return ( - <> - - - - - - - - - - {/* {this.galleryContentRoot && this.buildGalleryCallout()} */} - - ); - } else { - return ; - } - } - - public async initialize(): Promise { - const refreshTasks: Promise[] = []; - - this.galleryContentRoot = { - name: "Gallery", - path: "Gallery", - type: NotebookContentItemType.File, - }; - this.myNotebooksContentRoot = { - name: useNotebook.getState().notebookFolderName, - path: useNotebook.getState().notebookBasePath, - type: NotebookContentItemType.Directory, - }; - - // Only if notebook server is available we can refresh - if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) { - refreshTasks.push( - this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => { - this.triggerRender(); - this.traceMyNotebookTreeInfo(); - }), - ); - } - this.gitHubNotebooksContentRoot = { - name: ResourceTreeAdapter.GitHubReposTitle, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - }; - - return Promise.all(refreshTasks); - } - - public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { - if (this.gitHubNotebooksContentRoot) { - this.gitHubNotebooksContentRoot.children = []; - pinnedRepos?.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - const repoTreeItem: NotebookContentItem = { - name: repoFullName, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - children: [], - }; - - pinnedRepo.branches.forEach((branch) => { - repoTreeItem.children.push({ - name: branch.name, - path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), - type: NotebookContentItemType.Directory, - }); - }); - - this.gitHubNotebooksContentRoot.children.push(repoTreeItem); - }); - - this.triggerRender(); - } + return ; } private buildDataTree(): TreeNode { @@ -504,365 +370,6 @@ export class ResourceTreeAdapter implements ReactAdapter { return traverse(schema); } - private buildNotebooksTrees(): TreeNode { - let notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (this.galleryContentRoot) { - notebooksTree.children.push(this.buildGalleryNotebooksTree()); - } - - if (this.myNotebooksContentRoot) { - notebooksTree.children.push(this.buildMyNotebooksTree()); - } - - if (this.gitHubNotebooksContentRoot) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(this.buildGitHubNotebooksTree()); - } - - return notebooksTree; - } - - private buildGalleryCallout(): JSX.Element { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.triggerRender(); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.container.openGallery(); - this.triggerRender(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - } - - private buildGalleryNotebooksTree(): TreeNode { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => this.container.openGallery(), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; - }, - }; - } - - private buildMyNotebooksTree(): TreeNode { - const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.myNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - } - - private buildGitHubNotebooksTree(): TreeNode { - const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - gitHubNotebooksTree.contextMenu = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - this.container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - } - - private buildChildNodes( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createDirectoryContextMenu: boolean, - createFileContextMenu: boolean, - ): TreeNode[] { - if (!item || !item.children) { - return []; - } else { - return item.children.map((item) => { - const result = - item.type === NotebookContentItemType.Directory - ? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu) - : this.buildNotebookFileNode(item, onFileClick, createFileContextMenu); - result.timestamp = item.timestamp; - return result; - }); - } - } - - private buildNotebookFileNode( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createFileContextMenu: boolean, - ): TreeNode { - return { - label: item.name, - iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, - className: "notebookHeader", - onClick: () => onFileClick(item), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: createFileContextMenu && this.createFileContextMenu(item), - data: item, - }; - } - - private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { - let items: TreeNodeMenuItem[] = [ - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => this.container.renameNotebook(item), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined, - ); - }, - }, - { - label: "Copy to ...", - iconSrc: CopyIcon, - onClick: () => this.copyNotebook(item), - }, - { - label: "Download", - iconSrc: NotebookIcon, - onClick: () => this.container.downloadFile(item), - }, - ]; - - if (item.type === NotebookContentItemType.Notebook) { - items.push({ - label: "Publish to gallery", - iconSrc: PublishIcon, - onClick: async () => { - TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { - source: Source.ResourceTreeMenu, - }); - - const content = await this.container.readFile(item); - if (content) { - await this.container.publishNotebook(item.name, content); - } - }, - }); - } - - // "Copy to ..." isn't needed if github locations are not available - if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - items = items.filter((item) => item.label !== "Copy to ..."); - } - - return items; - } - - private copyNotebook = async (item: NotebookContentItem) => { - const content = await this.container.readFile(item); - if (content) { - this.container.copyNotebook(item.name, content); - } - }; - - private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { - let items: TreeNodeMenuItem[] = [ - { - label: "Refresh", - iconSrc: RefreshIcon, - onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()), - }, - { - label: "Delete", - iconSrc: DeleteIcon, - onClick: () => { - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined, - ); - }, - }, - { - label: "Rename", - iconSrc: NotebookIcon, - onClick: () => this.container.renameNotebook(item), - }, - { - label: "New Directory", - iconSrc: NewNotebookIcon, - onClick: () => this.container.onCreateDirectory(item), - }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => this.container.openUploadFilePanel(item), - }, - ]; - - //disallow renaming of temporary notebook workspace - if (item?.path === useNotebook.getState().notebookBasePath) { - items = items.filter((item) => item.label !== "Rename"); - } - - // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" - if (GitHubUtils.fromContentUri(item.path)) { - items = items.filter( - (item) => - item.label !== "Delete" && - item.label !== "Rename" && - item.label !== "New Directory" && - item.label !== "Upload File", - ); - } - - return items; - } - - private buildNotebookDirectoryNode( - item: NotebookContentItem, - onFileClick: (item: NotebookContentItem) => void, - createDirectoryContextMenu: boolean, - createFileContextMenu: boolean, - ): TreeNode { - return { - label: item.name, - iconSrc: undefined, - className: "notebookHeader", - isAlphaSorted: true, - isLeavesParentsSeparate: true, - onClick: () => { - if (!item.children) { - this.container.refreshContentItem(item).then(() => this.triggerRender()); - } - }, - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return ( - activeTab && - activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && - /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. - NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. - */ - (activeTab as any).notebookPath() === item.path - ); - }, - contextMenu: - createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath - ? this.createDirectoryContextMenu(item) - : undefined, - data: item, - children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), - }; - } - public triggerRender() { window.requestAnimationFrame(() => this.parameters(Date.now())); } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index f11b6df5c..1314b2b3c 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -245,7 +245,6 @@ export function downloadItem( }, "Cancel", undefined, - container.getDownloadModalConent(name), ); } export async function downloadNotebookItem( @@ -278,7 +277,6 @@ export async function downloadNotebookItem( metadata.untrusted = true; } - await container.importAndOpenContent(data.name, JSON.stringify(notebook)); logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`); const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id); From 81a5b7cb6d6011c9be922770581dcfe2021ee80d Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 30 Apr 2024 10:03:27 -0700 Subject: [PATCH 30/67] add shortcuts for the Items tab (#1827) * add shortcuts for the Items tab * Add shortcut to clear Items tab filter. --- src/Explorer/Tabs/DocumentsTab.html | 3 ++- src/Explorer/Tabs/DocumentsTab.ts | 33 ++++++++++++++++++++++++++++- src/Explorer/Tabs/TabsBase.ts | 2 ++ src/KeyboardShortcuts.tsx | 28 ++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/Explorer/Tabs/DocumentsTab.html b/src/Explorer/Tabs/DocumentsTab.html index 4283a661c..cfe1b2039 100644 --- a/src/Explorer/Tabs/DocumentsTab.html +++ b/src/Explorer/Tabs/DocumentsTab.html @@ -80,7 +80,8 @@ placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.' }, css: { placeholderVisible: filterContent().length === 0 }, - textInput: filterContent" + textInput: filterContent, + event: { keydown: onFilterKeyDown }" /> { super.onActivate(); + this.setKeyboardActions({ + [KeyboardAction.SEARCH]: () => { + this.onShowFilterClick(); + return true; + }, + [KeyboardAction.CLEAR_SEARCH]: () => { + this.filterContent(""); + this.refreshDocumentsGrid(true); + return true; + }, + }); + if (!this._documentsIterator) { try { await this.autoPopulateContent(); diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index 0425eac91..8b017f6bc 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -1,3 +1,4 @@ +import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import * as ThemeUtility from "../../Common/ThemeUtility"; @@ -107,6 +108,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { } public onActivate(): void { + clearKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); this.updateSelectedNode(); this.collection?.selectedSubnodeKind(this.tabKind); this.database?.selectedSubnodeKind(this.tabKind); diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 98f988038..2041662ee 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -17,8 +17,17 @@ export type KeyboardHandlerMap = Partial = { [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"], [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"], [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], + [KeyboardAction.SEARCH]: ["$mod+Shift+F"], + [KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"], }; interface KeyboardShortcutState { @@ -91,13 +104,24 @@ interface KeyboardShortcutState { setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void; } +export type KeyboardHandlerSetter = (handlers: KeyboardHandlerMap) => void; + /** * Defines the calling component as the manager of the keyboard actions for the given group. * @param group The group of keyboard actions to manage. * @returns A function that can be used to set the keyboard action handlers for the given group. */ -export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => - useKeyboardActionHandlers.getState().setHandlers(group, handlers); +export const useKeyboardActionGroup: (group: KeyboardActionGroup) => KeyboardHandlerSetter = + (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) => + useKeyboardActionHandlers.getState().setHandlers(group, handlers); + +/** + * Clears the keyboard action handlers for the given group. + * @param group The group of keyboard actions to clear. + */ +export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => { + useKeyboardActionHandlers.getState().setHandlers(group, {}); +}; const useKeyboardActionHandlers: UseStore = create((set, get) => ({ allHandlers: {}, From 298197b1b80e07282c93d0ee2b3608848d0c3af2 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Wed, 1 May 2024 07:21:50 -0700 Subject: [PATCH 31/67] Revert "First set of changes for Notebooks removal. (#1816)" (#1830) This reverts commit b023250e67e834ee8485670a1605973d0db88936. --- src/Contracts/ActionContracts.ts | 5 + .../SettingsComponent.test.tsx.snap | 16 + src/Explorer/Explorer.tsx | 612 +++++++++++++++++- src/Explorer/Notebook/NotebookManager.tsx | 27 +- src/Explorer/OpenActions/OpenActions.tsx | 12 + .../CopyNotebookPane/CopyNotebookPane.tsx | 154 +++++ .../CopyNotebookPaneComponent.tsx | 120 ++++ .../GitHubReposPanel.test.tsx.snap | 4 + .../StringInputPane.test.tsx.snap | 4 + .../QueryCopilotTab.test.tsx.snap | 4 + src/Explorer/SplashScreen/SplashScreen.tsx | 28 +- src/Explorer/Tabs/NotebookV2Tab.ts | 336 +++++++++- src/Explorer/Tree/ResourceTree.tsx | 432 ++++++++++++- src/Explorer/Tree/ResourceTreeAdapter.tsx | 497 +++++++++++++- src/Utils/GalleryUtils.ts | 2 + 15 files changed, 2242 insertions(+), 11 deletions(-) create mode 100644 src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx create mode 100644 src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx diff --git a/src/Contracts/ActionContracts.ts b/src/Contracts/ActionContracts.ts index cf4b66ed6..f8fc956e6 100644 --- a/src/Contracts/ActionContracts.ts +++ b/src/Contracts/ActionContracts.ts @@ -68,6 +68,10 @@ export interface OpenPane extends DataExplorerAction { paneKind: PaneKind | string; } +export interface OpenSampleNotebook extends DataExplorerAction { + path: string; +} + /** * The types of actions that the DataExplorer supports performing upon opening. */ @@ -76,4 +80,5 @@ export enum ActionType { OpenCollectionTab, OpenPane, TransmitCachedData, + OpenSampleNotebook, } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index b08834dd6..ab7abac11 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -29,6 +29,8 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -45,8 +47,10 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, }, @@ -103,6 +107,8 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -119,8 +125,10 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, }, @@ -216,6 +224,8 @@ exports[`SettingsComponent renders 1`] = ` "computedProperties": [Function], "conflictResolutionPolicy": [Function], "container": Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -232,8 +242,10 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, }, @@ -259,6 +271,8 @@ exports[`SettingsComponent renders 1`] = ` } explorer={ Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -275,8 +289,10 @@ exports[`SettingsComponent renders 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 368168a4a..4af478475 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,3 +1,4 @@ +import { Link } from "@fluentui/react/lib/Link"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { sendMessage } from "Common/MessageHandler"; import { Platform, configContext } from "ConfigContext"; @@ -15,7 +16,7 @@ import shallow from "zustand/shallow"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; -import { Areas, ConnectionStatusType, HttpStatusCodes, PoolIdType } from "../Common/Constants"; +import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; @@ -31,23 +32,34 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants" import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; +import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; +import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; +import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import { NotebookContentItem } from "./Notebook/NotebookContentItem"; +import * as FileSystemUtil from "./Notebook/FileSystemUtil"; +import { SnapshotRequest } from "./Notebook/NotebookComponent/types"; +import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import type NotebookManager from "./Notebook/NotebookManager"; +import { NotebookPaneContent } from "./Notebook/NotebookManager"; +import { NotebookUtil } from "./Notebook/NotebookUtil"; import { useNotebook } from "./Notebook/useNotebook"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; +import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; +import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; +import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import TabsBase from "./Tabs/TabsBase"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; @@ -75,6 +87,7 @@ export default class Explorer { // Notebooks public notebookManager?: NotebookManager; + private _isInitializingNotebooks: boolean; private notebookToImport: { name: string; content: string; @@ -86,6 +99,7 @@ export default class Explorer { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); + this._isInitializingNotebooks = false; this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); useNotebook.subscribe( @@ -191,10 +205,12 @@ export default class Explorer { container: this, resourceTree: this.resourceTree, refreshCommandBarButtons: () => this.refreshCommandBarButtons(), + refreshNotebookList: () => this.refreshNotebookList(), }); } this.refreshCommandBarButtons(); + this.refreshNotebookList(); } public openEnableSynapseLinkDialog(): void { @@ -357,6 +373,7 @@ export default class Explorer { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); + this.refreshNotebookList(); }; // Facade @@ -364,6 +381,19 @@ export default class Explorer { window.open(Constants.Urls.feedbackEmail, "_blank"); }; + public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { + if (!databaseAccount) { + throw new Error("No database account specified"); + } + + if (this._isInitializingNotebooks) { + return; + } + this._isInitializingNotebooks = true; + this.refreshNotebookList(); + this._isInitializingNotebooks = false; + } + public async allocateContainer(poolId: PoolIdType, mode?: string): Promise { const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false; const notebookServerInfo = shouldUseNotebookStates @@ -442,6 +472,8 @@ export default class Explorer { ? useNotebook.getState().setIsAllocating(false) : useQueryCopilot.getState().setIsAllocatingContainer(false); this.refreshCommandBarButtons(); + this.refreshNotebookList(); + this._isInitializingNotebooks = false; } } } @@ -478,6 +510,104 @@ export default class Explorer { .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } + public resetNotebookWorkspace(): void { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { + handleError( + "Attempt to reset notebook workspace, but notebook is not enabled", + "Explorer/resetNotebookWorkspace", + ); + return; + } + const dialogContent = useNotebook.getState().isPhoenixNotebooks + ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" + : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; + + const resetConfirmationDialogProps: DialogProps = { + isModal: true, + title: "Reset Workspace", + subText: dialogContent, + primaryButtonText: "OK", + secondaryButtonText: "Cancel", + onPrimaryButtonClick: this._resetNotebookWorkspace, + onSecondaryButtonClick: () => useDialog.getState().closeDialog(), + }; + useDialog.getState().openDialog(resetConfirmationDialogProps); + } + + private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { + if (!databaseAccount) { + return false; + } + try { + const { value: workspaces } = await listByDatabaseAccount( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + ); + return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); + return false; + } + } + + private _resetNotebookWorkspace = async () => { + useDialog.getState().closeDialog(); + const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); + let connectionStatus: ContainerConnectionInfo; + try { + const notebookServerInfo = useNotebook.getState().notebookServerInfo; + if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { + const error = "No server endpoint detected"; + Logger.logError(error, "NotebookContainerClient/resetWorkspace"); + logConsoleError(error); + return; + } + TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, + }); + if (useNotebook.getState().isPhoenixNotebooks) { + useTabs.getState().closeAllNotebookTabs(true); + connectionStatus = { + status: ConnectionStatusType.Connecting, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + } + const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); + if (connectionInfo?.status !== HttpStatusCodes.OK) { + throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); + } + if (!connectionInfo?.data?.phoenixServiceUrl) { + throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`); + } + if (useNotebook.getState().isPhoenixNotebooks) { + await this.setNotebookInfo(true, connectionInfo, connectionStatus); + useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); + } + logConsoleInfo("Successfully reset notebook workspace"); + TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, + }); + } catch (error) { + logConsoleError(`Failed to reset notebook workspace: ${error}`); + TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { + dataExplorerArea: Areas.Notebook, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }); + if (useNotebook.getState().isPhoenixNotebooks) { + connectionStatus = { + status: ConnectionStatusType.Failed, + }; + useNotebook.getState().resetContainerConnection(connectionStatus); + useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); + } + throw error; + } finally { + clearInProgressMessage(); + } + }; + private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -566,6 +696,406 @@ export default class Explorer { } } + public uploadFile( + name: string, + content: string, + parent: NotebookContentItem, + isGithubTree?: boolean, + ): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to upload notebook, but notebook is not enabled"; + handleError(error, "Explorer/uploadFile"); + throw new Error(error); + } + + const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree); + promise + .then(() => this.resourceTree.triggerRender()) + .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason))); + return promise; + } + + public async importAndOpen(path: string): Promise { + const name = NotebookUtil.getName(path); + const item = NotebookUtil.createNotebookContentItem(name, path, "file"); + const parent = this.resourceTree.myNotebooksContentRoot; + + if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { + const existingItem = _.find(parent.children, (node) => node.name === name); + if (existingItem) { + return this.openNotebook(existingItem); + } + + const content = await this.readFile(item); + const uploadedItem = await this.uploadFile(name, content, parent); + return this.openNotebook(uploadedItem); + } + + return Promise.resolve(false); + } + + public async importAndOpenContent(name: string, content: string): Promise { + const parent = this.resourceTree.myNotebooksContentRoot; + + if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) { + if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { + this.notebookToImport = undefined; // we don't want to try opening this notebook again + } + + const existingItem = _.find(parent.children, (node) => node.name === name); + if (existingItem) { + return this.openNotebook(existingItem); + } + + const uploadedItem = await this.uploadFile(name, content, parent); + return this.openNotebook(uploadedItem); + } + + this.notebookToImport = { name, content }; // we'll try opening this notebook later on + return Promise.resolve(false); + } + + public async publishNotebook( + name: string, + content: NotebookPaneContent, + notebookContentRef?: string, + onTakeSnapshot?: (request: SnapshotRequest) => void, + onClosePanel?: () => void, + ): Promise { + if (this.notebookManager) { + await this.notebookManager.openPublishNotebookPane( + name, + content, + notebookContentRef, + onTakeSnapshot, + onClosePanel, + ); + } + } + + public copyNotebook(name: string, content: string): void { + this.notebookManager?.openCopyNotebookPane(name, content); + } + + /** + * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. + * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. + * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder + * will not fetch its content if the children array exists (and has only one child which was manually created). + * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. + * + * @param name + * @param path + */ + public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { + return NotebookUtil.createNotebookContentItem(name, path, "file"); + } + + public async openNotebook(notebookContentItem: NotebookContentItem): Promise { + if (!notebookContentItem || !notebookContentItem.path) { + throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); + } + if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) { + await this.allocateContainer(PoolIdType.DefaultPoolId); + } + + const notebookTabs = useTabs + .getState() + .getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab) => + (tab as NotebookV2Tab).notebookPath && + FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path), + ) as NotebookV2Tab[]; + let notebookTab = notebookTabs && notebookTabs[0]; + + if (notebookTab) { + useTabs.getState().activateTab(notebookTab); + } else { + const options: NotebookTabOptions = { + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.NotebookV2, + node: undefined, + title: notebookContentItem.name, + tabPath: notebookContentItem.path, + collection: undefined, + masterKey: userContext.masterKey || "", + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: undefined, + container: this, + notebookContentItem, + }; + + try { + const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); + notebookTab = new NotebookTabV2.default(options); + useTabs.getState().activateNewTab(notebookTab); + } catch (reason) { + console.error("Import NotebookV2Tab failed!", reason); + return false; + } + } + + return true; + } + + public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to rename notebook, but notebook is not enabled"; + handleError(error, "Explorer/renameNotebook"); + throw new Error(error); + } + + // Don't delete if tab is open to avoid accidental deletion + const openedNotebookTabs = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { + return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); + }); + if (openedNotebookTabs.length > 0) { + useDialog + .getState() + .showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); + } else { + useSidePanel.getState().openSidePanel( + "Rename Notebook", + { + useSidePanel.getState().closeSidePanel(); + this.resourceTree.triggerRender(); + }} + inputLabel="Enter new notebook name" + submitButtonLabel="Rename" + errorMessage="Could not rename notebook" + inProgressMessage="Renaming notebook to" + successMessage="Renamed notebook to" + paneTitle="Rename Notebook" + defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")} + onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => + this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree) + } + notebookFile={notebookFile} + />, + ); + } + } + + public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to create notebook directory, but notebook is not enabled"; + handleError(error, "Explorer/onCreateDirectory"); + throw new Error(error); + } + + useSidePanel.getState().openSidePanel( + "Create new directory", + { + useSidePanel.getState().closeSidePanel(); + this.resourceTree.triggerRender(); + }} + errorMessage="Could not create directory " + inProgressMessage="Creating directory " + successMessage="Created directory " + inputLabel="Enter new directory name" + paneTitle="Create new directory" + submitButtonLabel="Create" + defaultInput="" + onSubmit={(notebookFile: NotebookContentItem, input: string): Promise => + this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree) + } + notebookFile={parent} + />, + ); + } + + public readFile(notebookFile: NotebookContentItem): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to read file, but notebook is not enabled"; + handleError(error, "Explorer/downloadFile"); + throw new Error(error); + } + + return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); + } + + public downloadFile(notebookFile: NotebookContentItem): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to download file, but notebook is not enabled"; + handleError(error, "Explorer/downloadFile"); + throw new Error(error); + } + + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); + + return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( + (content: string) => { + const blob = stringToBlob(content, "text/plain"); + if (navigator.msSaveBlob) { + // for IE and Edge + navigator.msSaveBlob(blob, notebookFile.name); + } else { + const downloadLink: HTMLAnchorElement = document.createElement("a"); + const url = URL.createObjectURL(blob); + downloadLink.href = url; + downloadLink.target = "_self"; + downloadLink.download = notebookFile.name; + + // for some reason, FF displays the download prompt only when + // the link is added to the dom so we add and remove it + document.body.appendChild(downloadLink); + downloadLink.click(); + downloadLink.remove(); + } + + clearMessage(); + }, + (error) => { + logConsoleError(`Could not download notebook ${getErrorMessage(error)}`); + clearMessage(); + }, + ); + } + + private refreshNotebookList = async (): Promise => { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + return; + } + + await this.resourceTree.initialize(); + await useNotebook.getState().initializeNotebooksTree(this.notebookManager); + + this.notebookManager?.refreshPinnedRepos(); + if (this.notebookToImport) { + this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); + } + }; + + public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to delete notebook file, but notebook is not enabled"; + handleError(error, "Explorer/deleteNotebookFile"); + throw new Error(error); + } + + // Don't delete if tab is open to avoid accidental deletion + const openedNotebookTabs = useTabs + .getState() + .getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => { + return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); + }); + if (openedNotebookTabs.length > 0) { + useDialog + .getState() + .showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); + return Promise.reject(); + } + + if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { + useDialog.getState().openDialog({ + isModal: true, + title: "Unable to delete file", + subText: "Directory is not empty.", + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: () => useDialog.getState().closeDialog(), + onSecondaryButtonClick: undefined, + }); + return Promise.reject(); + } + + return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then( + () => logConsoleInfo(`Successfully deleted: ${item.path}`), + (reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`), + ); + } + + /** + * This creates a new notebook file, then opens the notebook + */ + public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { + if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to create new notebook, but notebook is not enabled"; + handleError(error, "Explorer/onNewNotebookClicked"); + throw new Error(error); + } + if (useNotebook.getState().isPhoenixNotebooks) { + if (isGithubTree) { + await this.allocateContainer(PoolIdType.DefaultPoolId); + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.createNewNoteBook(parent, isGithubTree); + } else { + useDialog.getState().showOkCancelModalDialog( + Notebook.newNotebookModalTitle, + undefined, + "Create", + async () => { + await this.allocateContainer(PoolIdType.DefaultPoolId); + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.createNewNoteBook(parent, isGithubTree); + }, + "Cancel", + undefined, + this.getNewNoteWarningText(), + ); + } + } else { + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.createNewNoteBook(parent, isGithubTree); + } + } + + private getNewNoteWarningText(): JSX.Element { + return ( + <> +

{Notebook.newNotebookModalContent1}

+
+

+ {Notebook.newNotebookModalContent2} + + {Notebook.learnMore} + +

+ + ); + } + + private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { + const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { + dataExplorerArea: Constants.Areas.Notebook, + }); + + this.notebookManager?.notebookContentClient + .createNewNotebookFile(parent, isGithubTree) + .then((newFile: NotebookContentItem) => { + logConsoleInfo(`Successfully created: ${newFile.name}`); + TelemetryProcessor.traceSuccess( + Action.CreateNewNotebook, + { + dataExplorerArea: Constants.Areas.Notebook, + }, + startKey, + ); + return this.openNotebook(newFile); + }) + .then(() => this.resourceTree.triggerRender()) + .catch((error) => { + const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; + logConsoleError(errorMessage); + TelemetryProcessor.traceFailure( + Action.CreateNewNotebook, + { + dataExplorerArea: Constants.Areas.Notebook, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey, + ); + }) + .finally(clearInProgressMessage); + } + // TODO: Delete this function when ResourceTreeAdapter is removed. public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { @@ -722,6 +1252,32 @@ export default class Explorer { } } + public async handleOpenFileAction(path: string): Promise { + if (useNotebook.getState().isPhoenixNotebooks === undefined) { + await useNotebook.getState().getPhoenixStatus(); + } + if (useNotebook.getState().isPhoenixNotebooks) { + await this.allocateContainer(PoolIdType.DefaultPoolId); + } + + // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb + // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly + // calling GitHub. For now convert this url to a raw url and download content. + const gitHubInfo = fromContentUri(path); + if (gitHubInfo) { + const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); + const response = await fetch(rawUrl); + if (response.status === Constants.HttpStatusCodes.OK) { + this.notebookToImport = { + name: NotebookUtil.getName(path), + content: await response.text(), + }; + + this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); + } + } + } + public openUploadItemsPanePane(): void { useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } @@ -731,6 +1287,54 @@ export default class Explorer { .openSidePanel("Input parameters", ); } + public openUploadFilePanel(parent?: NotebookContentItem): void { + if (useNotebook.getState().isPhoenixNotebooks) { + useDialog.getState().showOkCancelModalDialog( + Notebook.newNotebookUploadModalTitle, + undefined, + "Upload", + async () => { + await this.allocateContainer(PoolIdType.DefaultPoolId); + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.uploadFilePanel(parent); + }, + "Cancel", + undefined, + this.getNewNoteWarningText(), + ); + } else { + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.uploadFilePanel(parent); + } + } + + private uploadFilePanel(parent?: NotebookContentItem): void { + useSidePanel + .getState() + .openSidePanel( + "Upload file to notebook server", + this.uploadFile(name, content, parent)} />, + ); + } + + public getDownloadModalConent(fileName: string): JSX.Element { + if (useNotebook.getState().isPhoenixNotebooks) { + return ( + <> +

{Notebook.galleryNotebookDownloadContent1}

+
+

+ {Notebook.galleryNotebookDownloadContent2} + + {Notebook.learnMore} + +

+ + ); + } + return

Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook.

; + } + public async refreshExplorer(): Promise { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { userContext.authType === AuthType.ResourceToken @@ -755,6 +1359,10 @@ export default class Explorer { dataExplorerArea: Constants.Areas.Notebook, }); + if (useNotebook.getState().isPhoenixNotebooks) { + await this.initNotebooks(userContext.databaseAccount); + } + await this.refreshSampleData(); } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 45afe6061..3ccbefcaf 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -12,13 +12,15 @@ import * as Logger from "../../Common/Logger"; import { GitHubClient } from "../../GitHub/GitHubClient"; import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { JunoClient } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; import { getFullName } from "../../Utils/UserUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; +import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; @@ -38,6 +40,7 @@ export interface NotebookManagerOptions { container: Explorer; resourceTree: ResourceTreeAdapter; refreshCommandBarButtons: () => void; + refreshNotebookList: () => void; } export default class NotebookManager { @@ -78,6 +81,10 @@ export default class NotebookManager { contents.JupyterContentProvider, ); + this.notebookClient = new NotebookContainerClient(() => + this.params.container.initNotebooks(userContext?.databaseAccount), + ); + this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider); this.gitHubOAuthService.getTokenObservable().subscribe((token) => { @@ -99,9 +106,11 @@ export default class NotebookManager { } this.params.refreshCommandBarButtons(); + this.params.refreshNotebookList(); }); this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { + this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.triggerRender(); useNotebook.getState().initializeGitHubRepos(pinnedRepos); }); @@ -140,6 +149,22 @@ export default class NotebookManager { ); } + public openCopyNotebookPane(name: string, content: string): void { + const { container } = this.params; + useSidePanel + .getState() + .openSidePanel( + "Copy Notebook", + , + ); + } + // Octokit's error handler uses any // eslint-disable-next-line @typescript-eslint/no-explicit-any private onGitHubClientError = (error: any): void => { diff --git a/src/Explorer/OpenActions/OpenActions.tsx b/src/Explorer/OpenActions/OpenActions.tsx index e2059cdba..f3ef288c8 100644 --- a/src/Explorer/OpenActions/OpenActions.tsx +++ b/src/Explorer/OpenActions/OpenActions.tsx @@ -195,5 +195,17 @@ export function handleOpenAction( return true; } + if ( + action.actionType === ActionContracts.ActionType.OpenSampleNotebook || + action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook] + ) { + openFile(action as ActionContracts.OpenSampleNotebook, explorer); + return true; + } + return false; } + +function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) { + explorer.handleOpenFileAction(decodeURIComponent(action.path)); +} diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx new file mode 100644 index 000000000..0f7927b3b --- /dev/null +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -0,0 +1,154 @@ +import { IDropdownOption } from "@fluentui/react"; +import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; +import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants"; +import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; +import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; +import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; +import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; +import { useSidePanel } from "../../../hooks/useSidePanel"; +import Explorer from "../../Explorer"; +import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; +import { useNotebook } from "../../Notebook/useNotebook"; +import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; +import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; + +interface Location { + type: "MyNotebooks" | "GitHub"; + + // GitHub + owner?: string; + repo?: string; + branch?: string; +} +export interface CopyNotebookPanelProps { + name: string; + content: string; + container: Explorer; + junoClient: JunoClient; + gitHubOAuthService: GitHubOAuthService; +} + +export const CopyNotebookPane: FunctionComponent = ({ + name, + content, + container, + junoClient, + gitHubOAuthService, +}: CopyNotebookPanelProps) => { + const closeSidePanel = useSidePanel((state) => state.closeSidePanel); + const [isExecuting, setIsExecuting] = useState(); + const [formError, setFormError] = useState(""); + const [pinnedRepos, setPinnedRepos] = useState(); + const [selectedLocation, setSelectedLocation] = useState(); + + useEffect(() => { + open(); + }, []); + + const open = async (): Promise => { + if (gitHubOAuthService.isLoggedIn()) { + const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope); + if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { + handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit"); + } + + if (response.data?.length > 0) { + setPinnedRepos(response.data); + } + } + }; + + const submit = async (): Promise => { + let destination: string = selectedLocation?.type; + let clearMessage: () => void; + setIsExecuting(true); + + try { + if (!selectedLocation) { + throw new Error(`No location selected`); + } + + if (selectedLocation.type === "GitHub") { + destination = `${destination} - ${GitHubUtils.toRepoFullName( + selectedLocation.owner, + selectedLocation.repo, + )} - ${selectedLocation.branch}`; + } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) { + destination = useNotebook.getState().notebookFolderName; + } + + clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); + + const notebookContentItem = await copyNotebook(selectedLocation); + if (!notebookContentItem) { + throw new Error(`Failed to upload ${name}`); + } + + NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`); + closeSidePanel(); + } catch (error) { + const errorMessage = getErrorMessage(error); + setFormError(`Failed to copy ${name} to ${destination}`); + handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); + } finally { + clearMessage && clearMessage(); + setIsExecuting(false); + } + }; + + const copyNotebook = async (location: Location): Promise => { + let parent: NotebookContentItem; + let isGithubTree: boolean; + switch (location.type) { + case "MyNotebooks": + parent = { + name: useNotebook.getState().notebookFolderName, + path: useNotebook.getState().notebookBasePath, + type: NotebookContentItemType.Directory, + }; + isGithubTree = false; + if (useNotebook.getState().isPhoenixNotebooks) { + await container.allocateContainer(PoolIdType.DefaultPoolId); + } + break; + + case "GitHub": + parent = { + name: selectedLocation.branch, + path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""), + type: NotebookContentItemType.Directory, + }; + isGithubTree = true; + break; + + default: + throw new Error(`Unsupported location type ${location.type}`); + } + + return container.uploadFile(name, content, parent, isGithubTree); + }; + + const onDropDownChange = (_: FormEvent, option?: IDropdownOption): void => { + setSelectedLocation(option?.data); + }; + + const props: RightPaneFormProps = { + formError, + isExecuting: isExecuting, + submitButtonText: "OK", + onSubmit: () => submit(), + }; + + const copyNotebookPaneProps: CopyNotebookPaneProps = { + name, + pinnedRepos, + onDropDownChange: onDropDownChange, + }; + + return ( + + + + ); +}; diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx new file mode 100644 index 000000000..5cd0cfdc1 --- /dev/null +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx @@ -0,0 +1,120 @@ +import { + Dropdown, + IDropdownOption, + IDropdownProps, + IRenderFunction, + ISelectableOption, + Label, + SelectableOptionMenuItemType, + Stack, + Text, +} from "@fluentui/react"; +import { GitHubReposTitle } from "Explorer/Tree/ResourceTree"; +import React, { FormEvent, FunctionComponent } from "react"; +import { IPinnedRepo } from "../../../Juno/JunoClient"; +import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import { useNotebook } from "../../Notebook/useNotebook"; + +interface Location { + type: "MyNotebooks" | "GitHub"; + + // GitHub + owner?: string; + repo?: string; + branch?: string; +} + +export interface CopyNotebookPaneProps { + name: string; + pinnedRepos: IPinnedRepo[]; + onDropDownChange: (_: FormEvent, option?: IDropdownOption) => void; +} + +export const CopyNotebookPaneComponent: FunctionComponent = ({ + name, + pinnedRepos, + onDropDownChange, +}: CopyNotebookPaneProps) => { + const BranchNameWhiteSpace = " "; + + const onRenderDropDownTitle: IRenderFunction = (options: IDropdownOption[]): JSX.Element => { + return {options.length && options[0].title}; + }; + + const onRenderDropDownOption: IRenderFunction = (option: ISelectableOption): JSX.Element => { + return {option.text}; + }; + + const getDropDownOptions = (): IDropdownOption[] => { + const options: IDropdownOption[] = []; + options.push({ + key: "MyNotebooks-Item", + text: useNotebook.getState().notebookFolderName, + title: useNotebook.getState().notebookFolderName, + data: { + type: "MyNotebooks", + } as Location, + }); + + if (pinnedRepos && pinnedRepos.length > 0) { + options.push({ + key: "GitHub-Header-Divider", + text: undefined, + itemType: SelectableOptionMenuItemType.Divider, + }); + + options.push({ + key: "GitHub-Header", + text: GitHubReposTitle, + itemType: SelectableOptionMenuItemType.Header, + }); + + pinnedRepos.forEach((pinnedRepo) => { + const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); + options.push({ + key: `GitHub-Repo-${repoFullName}`, + text: repoFullName, + disabled: true, + }); + + pinnedRepo.branches.forEach((branch) => + options.push({ + key: `GitHub-Repo-${repoFullName}-${branch.name}`, + text: `${BranchNameWhiteSpace}${branch.name}`, + title: `${repoFullName} - ${branch.name}`, + data: { + type: "GitHub", + owner: pinnedRepo.owner, + repo: pinnedRepo.name, + branch: branch.name, + } as Location, + }), + ); + }); + } + + return options; + }; + const dropDownProps: IDropdownProps = { + label: "Location", + ariaLabel: "Location", + placeholder: "Select an option", + onRenderTitle: onRenderDropDownTitle, + onRenderOption: onRenderDropDownOption, + options: getDropDownOptions(), + onChange: onDropDownChange, + }; + + return ( +
+ + + + {name} + + + + +
+ ); +}; diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index aa2137c42..4a3a8942e 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -17,6 +17,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` addRepoProps={ Object { "container": Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -33,8 +35,10 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, }, diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 566808f4b..8054abe19 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -7,6 +7,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` errorMessage="Could not create directory " explorer={ Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -23,8 +25,10 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 6d875fe2e..26b52ff90 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -22,6 +22,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = databaseId="CopilotSampleDb" explorer={ Explorer { + "_isInitializingNotebooks": false, + "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -38,8 +40,10 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = "queriesClient": QueriesClient { "container": [Circular], }, + "refreshNotebookList": [Function], "resourceTree": ResourceTreeAdapter { "container": [Circular], + "copyNotebook": [Function], "parameters": [Function], }, } diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index e9477e088..f4ebb9cd0 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -25,9 +25,11 @@ import * as React from "react"; import ConnectIcon from "../../../images/Connect_color.svg"; import ContainersIcon from "../../../images/Containers.svg"; import LinkIcon from "../../../images/Link_blue.svg"; +import NotebookColorIcon from "../../../images/Notebooks.svg"; import PowerShellIcon from "../../../images/PowerShell.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; +import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import * as Constants from "../../Common/Constants"; import { userContext } from "../../UserContext"; @@ -408,6 +410,14 @@ export class SplashScreen extends React.Component { }, }; heroes.push(launchQuickstartBtn); + } else if (useNotebook.getState().isPhoenixNotebooks) { + const newNotebookBtn = { + iconSrc: NotebookColorIcon, + title: "New notebook", + description: "Visualize your data stored in Azure Cosmos DB", + onClick: () => this.container.onNewNotebookClicked(), + }; + heroes.push(newNotebookBtn); } heroes.push(this.getShellCard()); @@ -483,12 +493,28 @@ export class SplashScreen extends React.Component { }; } + private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) { + return { + info: path, + iconSrc: NotebookIcon, + title: name, + description: "Notebook", + onClick: () => { + const notebookItem = this.container.createNotebookContentItemFile(name, path); + notebookItem && this.container.openNotebook(notebookItem); + }, + }; + } + private createRecentItems(): SplashScreenItem[] { return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => { switch (activity.type) { default: { - throw new Error(`Unknown activity: ${activity}`); + const unknownActivity: never = activity; + throw new Error(`Unknown activity: ${unknownActivity}`); } + case MostRecentActivity.Type.OpenNotebook: + return this.decorateOpenNotebookActivity(activity); case MostRecentActivity.Type.OpenCollection: return this.decorateOpenCollectionActivity(activity); diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index fadb43258..92e0e6958 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -1,11 +1,31 @@ +import { stringifyNotebook, toJS } from "@nteract/commutable"; import * as ko from "knockout"; import * as Q from "q"; +import { userContext } from "UserContext"; +import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; +import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; +import CutIcon from "../../../images/notebook/Notebook-cut.svg"; +import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg"; +import PasteIcon from "../../../images/notebook/Notebook-paste.svg"; +import RestartIcon from "../../../images/notebook/Notebook-restart.svg"; +import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg"; +import RunIcon from "../../../images/notebook/Notebook-run.svg"; +import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg"; +import SaveIcon from "../../../images/save-cosmos.svg"; +import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore"; +import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { useDialog } from "../Controls/Dialog"; +import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; +import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; +import * as CdbActions from "../Notebook/NotebookComponent/actions"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; +import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types"; import { NotebookContentItem } from "../Notebook/NotebookContentItem"; +import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; @@ -70,7 +90,275 @@ export default class NotebookTabV2 extends NotebookTabBase { } protected getTabsButtons(): CommandButtonComponentProps[] { - return []; + const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); + const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted(); + + const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined; + + const saveLabel = "Save"; + const copyToLabel = "Copy to ..."; + const publishLabel = "Publish to gallery"; + const kernelLabel = "No Kernel"; + const runLabel = "Run"; + const runActiveCellLabel = "Run Active Cell"; + const runAllLabel = "Run All"; + const interruptKernelLabel = "Interrupt Kernel"; + const killKernelLabel = "Halt Kernel"; + const restartKernelLabel = "Restart Kernel"; + const clearLabel = "Clear outputs"; + const newCellLabel = "New Cell"; + const cellTypeLabel = "Cell Type"; + const codeLabel = "Code"; + const markdownLabel = "Markdown"; + const rawLabel = "Raw"; + const copyLabel = "Copy"; + const cutLabel = "Cut"; + const pasteLabel = "Paste"; + const cellCodeType = "code"; + const cellMarkdownType = "markdown"; + const cellRawType = "raw"; + + const saveButtonChildren = []; + if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + saveButtonChildren.push({ + iconName: copyToLabel, + onCommandClick: () => this.copyNotebook(), + commandButtonLabel: copyToLabel, + hasPopup: false, + disabled: false, + ariaLabel: copyToLabel, + }); + } + + if (userContext.features.publicGallery) { + saveButtonChildren.push({ + iconName: "PublishContent", + onCommandClick: async () => await this.publishToGallery(), + commandButtonLabel: publishLabel, + hasPopup: false, + disabled: false, + ariaLabel: publishLabel, + }); + } + + let buttons: CommandButtonComponentProps[] = [ + { + iconSrc: SaveIcon, + iconAlt: saveLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookSave(), + commandButtonLabel: saveLabel, + hasPopup: false, + disabled: false, + ariaLabel: saveLabel, + children: saveButtonChildren.length && [ + { + iconName: "Save", + onCommandClick: () => this.notebookComponentAdapter.notebookSave(), + commandButtonLabel: saveLabel, + hasPopup: false, + disabled: false, + ariaLabel: saveLabel, + }, + ...saveButtonChildren, + ], + }, + { + iconSrc: null, + iconAlt: kernelLabel, + onCommandClick: () => {}, + commandButtonLabel: null, + hasPopup: false, + disabled: availableKernels.length < 1, + isDropdown: true, + dropdownPlaceholder: kernelLabel, + dropdownSelectedKey: this.notebookComponentAdapter.getSelectedKernelName(), //this.currentKernelName, + dropdownWidth: 100, + children: availableKernels.map( + (kernel: KernelSpecsDisplay) => + ({ + iconSrc: null, + iconAlt: kernel.displayName, + onCommandClick: () => this.notebookComponentAdapter.notebookChangeKernel(kernel.name), + commandButtonLabel: kernel.displayName, + dropdownItemKey: kernel.name, + hasPopup: false, + disabled: false, + ariaLabel: kernel.displayName, + }) as CommandButtonComponentProps, + ), + ariaLabel: kernelLabel, + }, + { + iconSrc: RunIcon, + iconAlt: runLabel, + onCommandClick: () => { + this.notebookComponentAdapter.notebookRunAndAdvance(); + this.traceTelemetry(Action.ExecuteCell); + }, + commandButtonLabel: runLabel, + tooltipText: runBtnTooltip, + ariaLabel: runLabel, + hasPopup: false, + disabled: isNotebookUntrusted, + children: [ + { + iconSrc: RunIcon, + iconAlt: runActiveCellLabel, + onCommandClick: () => { + this.notebookComponentAdapter.notebookRunAndAdvance(); + this.traceTelemetry(Action.ExecuteCell); + }, + commandButtonLabel: runActiveCellLabel, + hasPopup: false, + disabled: false, + ariaLabel: runActiveCellLabel, + }, + { + iconSrc: RunAllIcon, + iconAlt: runAllLabel, + onCommandClick: () => { + this.notebookComponentAdapter.notebookRunAll(); + this.traceTelemetry(Action.ExecuteAllCells); + }, + commandButtonLabel: runAllLabel, + hasPopup: false, + disabled: false, + ariaLabel: runAllLabel, + }, + { + iconSrc: InterruptKernelIcon, + iconAlt: interruptKernelLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookInterruptKernel(), + commandButtonLabel: interruptKernelLabel, + hasPopup: false, + disabled: false, + ariaLabel: interruptKernelLabel, + }, + { + iconSrc: KillKernelIcon, + iconAlt: killKernelLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookKillKernel(), + commandButtonLabel: killKernelLabel, + hasPopup: false, + disabled: false, + ariaLabel: killKernelLabel, + }, + { + iconSrc: RestartIcon, + iconAlt: restartKernelLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookRestartKernel(), + commandButtonLabel: restartKernelLabel, + hasPopup: false, + disabled: false, + ariaLabel: restartKernelLabel, + }, + ], + }, + { + iconSrc: ClearAllOutputsIcon, + iconAlt: clearLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookClearAllOutputs(), + commandButtonLabel: clearLabel, + hasPopup: false, + disabled: false, + ariaLabel: clearLabel, + }, + { + iconSrc: NewCellIcon, + iconAlt: newCellLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookInsertBelow(), + commandButtonLabel: newCellLabel, + ariaLabel: newCellLabel, + hasPopup: false, + disabled: false, + }, + CommandBarComponentButtonFactory.createDivider(), + { + iconSrc: null, + iconAlt: null, + onCommandClick: () => {}, + commandButtonLabel: null, + ariaLabel: cellTypeLabel, + hasPopup: false, + disabled: false, + isDropdown: true, + dropdownPlaceholder: cellTypeLabel, + dropdownSelectedKey: this.notebookComponentAdapter.getActiveCellTypeStr(), + dropdownWidth: 110, + children: [ + { + iconSrc: null, + iconAlt: null, + onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellCodeType), + commandButtonLabel: codeLabel, + ariaLabel: codeLabel, + dropdownItemKey: cellCodeType, + hasPopup: false, + disabled: false, + }, + { + iconSrc: null, + iconAlt: null, + onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellMarkdownType), + commandButtonLabel: markdownLabel, + ariaLabel: markdownLabel, + dropdownItemKey: cellMarkdownType, + hasPopup: false, + disabled: false, + }, + { + iconSrc: null, + iconAlt: null, + onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellRawType), + commandButtonLabel: rawLabel, + ariaLabel: rawLabel, + dropdownItemKey: cellRawType, + hasPopup: false, + disabled: false, + }, + ], + }, + { + iconSrc: CopyIcon, + iconAlt: copyLabel, + onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), + commandButtonLabel: copyLabel, + ariaLabel: copyLabel, + hasPopup: false, + disabled: false, + children: [ + { + iconSrc: CopyIcon, + iconAlt: copyLabel, + onCommandClick: () => this.notebookComponentAdapter.notebokCopy(), + commandButtonLabel: copyLabel, + ariaLabel: copyLabel, + hasPopup: false, + disabled: false, + }, + { + iconSrc: CutIcon, + iconAlt: cutLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookCut(), + commandButtonLabel: cutLabel, + ariaLabel: cutLabel, + hasPopup: false, + disabled: false, + }, + { + iconSrc: PasteIcon, + iconAlt: pasteLabel, + onCommandClick: () => this.notebookComponentAdapter.notebookPaste(), + commandButtonLabel: pasteLabel, + ariaLabel: pasteLabel, + hasPopup: false, + disabled: false, + }, + ], + }, + // TODO: Uncomment when undo/redo is reimplemented in nteract + ]; + return buttons; } protected buildCommandBarOptions(): void { @@ -94,4 +382,50 @@ export default class NotebookTabV2 extends NotebookTabBase { sparkClusterConnectionInfo, ); } + + private publishToGallery = async () => { + TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { + source: Source.CommandBarMenu, + }); + + const notebookReduxStore = NotebookTabV2.clientManager.getStore(); + const unsubscribe = notebookReduxStore.subscribe(() => { + const cdbState = (notebookReduxStore.getState() as CdbAppState).cdb; + useNotebookSnapshotStore.setState({ + snapshot: cdbState.notebookSnapshot?.imageSrc, + error: cdbState.notebookSnapshotError, + }); + }); + + const notebookContent = this.notebookComponentAdapter.getContent(); + const notebookContentRef = this.notebookComponentAdapter.contentRef; + const onPanelClose = (): void => { + unsubscribe(); + useNotebookSnapshotStore.setState({ + snapshot: undefined, + error: undefined, + }); + notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(undefined)); + }; + + await this.container.publishNotebook( + notebookContent.name, + notebookContent.content, + notebookContentRef, + (request: SnapshotRequest) => notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(request)), + onPanelClose, + ); + }; + + private copyNotebook = () => { + const notebookContent = this.notebookComponentAdapter.getContent(); + let content: string; + if (typeof notebookContent.content === "string") { + content = notebookContent.content; + } else { + content = stringifyNotebook(toJS(notebookContent.content)); + } + + this.container.copyNotebook(notebookContent.name, content); + }; } diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 0933eac7b..b5f759534 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,23 +1,42 @@ +import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; +import GalleryIcon from "../../../images/GalleryIcon.svg"; +import DeleteIcon from "../../../images/delete.svg"; +import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; +import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; +import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; +import FileIcon from "../../../images/notebook/file-cosmos.svg"; +import PublishIcon from "../../../images/notebook/publish_content.svg"; +import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; +import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; +import * as GitHubUtils from "../../Utils/GitHubUtils"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; -import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; +import { useDialog } from "../Controls/Dialog"; +import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; +import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; +import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; +import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -26,21 +45,391 @@ import StoredProcedure from "./StoredProcedure"; import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; +export const MyNotebooksTitle = "My Notebooks"; +export const GitHubReposTitle = "GitHub repos"; + interface ResourceTreeProps { container: Explorer; } export const ResourceTree: React.FC = ({ container }: ResourceTreeProps): JSX.Element => { const databases = useDatabases((state) => state.databases); - const { isNotebookEnabled } = useNotebook( + const { + isNotebookEnabled, + myNotebooksContentRoot, + galleryContentRoot, + gitHubNotebooksContentRoot, + updateNotebookItem, + } = useNotebook( (state) => ({ isNotebookEnabled: state.isNotebookEnabled, + myNotebooksContentRoot: state.myNotebooksContentRoot, + galleryContentRoot: state.galleryContentRoot, + gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot, + updateNotebookItem: state.updateNotebookItem, }), shallow, ); - const { refreshActiveTab } = useTabs(); + const { activeTab, refreshActiveTab } = useTabs(); const showScriptNodes = configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); + const pseudoDirPath = "PsuedoDir"; + + const buildGalleryCallout = (): JSX.Element => { + if ( + LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && + LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) + ) { + return undefined; + } + + const calloutProps: ICalloutProps = { + calloutMaxWidth: 350, + ariaLabel: "New gallery", + role: "alertdialog", + gapSpace: 0, + target: ".galleryHeader", + directionalHint: DirectionalHint.leftTopEdge, + onDismiss: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + }, + setInitialFocus: true, + }; + + const openGalleryProps: ILinkProps = { + onClick: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + container.openGallery(); + }, + }; + + return ( + + + + New gallery + + + Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other + contributors. + + Open gallery + + + ); + }; + + const buildNotebooksTree = (): TreeNode => { + const notebooksTree: TreeNode = { + label: undefined, + isExpanded: true, + children: [], + }; + + if (!useNotebook.getState().isPhoenixNotebooks) { + notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); + } else { + if (galleryContentRoot) { + notebooksTree.children.push(buildGalleryNotebooksTree()); + } + + if ( + myNotebooksContentRoot && + useNotebook.getState().isPhoenixNotebooks && + useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected + ) { + notebooksTree.children.push(buildMyNotebooksTree()); + } + if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + // collapse all other notebook nodes + notebooksTree.children.forEach((node) => (node.isExpanded = false)); + notebooksTree.children.push(buildGitHubNotebooksTree(true)); + } + } + return notebooksTree; + }; + + const buildNotebooksTemporarilyDownTree = (): TreeNode => { + return { + label: Notebook.temporarilyDownMsg, + className: "clickDisabled", + }; + }; + + const buildGalleryNotebooksTree = (): TreeNode => { + return { + label: "Gallery", + iconSrc: GalleryIcon, + className: "notebookHeader galleryHeader", + onClick: () => container.openGallery(), + isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, + }; + }; + + const buildMyNotebooksTree = (): TreeNode => { + const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( + myNotebooksContentRoot, + (item: NotebookContentItem) => { + container.openNotebook(item); + }, + ); + + myNotebooksTree.isExpanded = true; + myNotebooksTree.isAlphaSorted = true; + // Remove "Delete" menu item from context menu + myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); + return myNotebooksTree; + }; + + const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { + const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( + gitHubNotebooksContentRoot, + (item: NotebookContentItem) => { + container.openNotebook(item); + }, + true, + ); + const manageGitContextMenu: TreeNodeMenuItem[] = [ + { + label: "Manage GitHub settings", + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Manage GitHub settings", + , + ), + }, + { + label: "Disconnect from GitHub", + onClick: () => { + TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { + dataExplorerArea: Areas.Notebook, + }); + container.notebookManager?.gitHubOAuthService.logout(); + }, + }, + ]; + gitHubNotebooksTree.contextMenu = manageGitContextMenu; + gitHubNotebooksTree.isExpanded = true; + gitHubNotebooksTree.isAlphaSorted = true; + + return gitHubNotebooksTree; + }; + + const buildChildNodes = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean, + ): TreeNode[] => { + if (!item || !item.children) { + return []; + } else { + return item.children.map((item) => { + const result = + item.type === NotebookContentItemType.Directory + ? buildNotebookDirectoryNode(item, onFileClick, isGithubTree) + : buildNotebookFileNode(item, onFileClick, isGithubTree); + result.timestamp = item.timestamp; + return result; + }); + } + }; + + const buildNotebookFileNode = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean, + ): TreeNode => { + return { + label: item.name, + iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, + className: "notebookHeader", + onClick: () => onFileClick(item), + isSelected: () => { + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: createFileContextMenu(container, item, isGithubTree), + data: item, + }; + }; + + const createFileContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean, + ): TreeNodeMenuItem[] => { + let items: TreeNodeMenuItem[] = [ + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => container.renameNotebook(item, isGithubTree), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => container.deleteNotebookFile(item, isGithubTree), + "Cancel", + undefined, + ); + }, + }, + { + label: "Copy to ...", + iconSrc: CopyIcon, + onClick: () => copyNotebook(container, item), + }, + { + label: "Download", + iconSrc: NotebookIcon, + onClick: () => container.downloadFile(item), + }, + ]; + + if (item.type === NotebookContentItemType.Notebook && userContext.features.publicGallery) { + items.push({ + label: "Publish to gallery", + iconSrc: PublishIcon, + onClick: async () => { + TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { + source: Source.ResourceTreeMenu, + }); + + const content = await container.readFile(item); + if (content) { + await container.publishNotebook(item.name, content); + } + }, + }); + } + + // "Copy to ..." isn't needed if github locations are not available + if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + items = items.filter((item) => item.label !== "Copy to ..."); + } + + return items; + }; + + const copyNotebook = async (container: Explorer, item: NotebookContentItem) => { + const content = await container.readFile(item); + if (content) { + container.copyNotebook(item.name, content); + } + }; + + const createDirectoryContextMenu = ( + container: Explorer, + item: NotebookContentItem, + isGithubTree?: boolean, + ): TreeNodeMenuItem[] => { + let items: TreeNodeMenuItem[] = [ + { + label: "Refresh", + iconSrc: RefreshIcon, + onClick: () => loadSubitems(item, isGithubTree), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => container.deleteNotebookFile(item, isGithubTree), + "Cancel", + undefined, + ); + }, + }, + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => container.renameNotebook(item, isGithubTree), + }, + { + label: "New Directory", + iconSrc: NewNotebookIcon, + onClick: () => container.onCreateDirectory(item, isGithubTree), + }, + { + label: "Upload File", + iconSrc: NewNotebookIcon, + onClick: () => container.openUploadFilePanel(item), + }, + ]; + + //disallow renaming of temporary notebook workspace + if (item?.path === useNotebook.getState().notebookBasePath) { + items = items.filter((item) => item.label !== "Rename"); + } + + // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" + if (GitHubUtils.fromContentUri(item.path)) { + items = items.filter( + (item) => + item.label !== "Delete" && + item.label !== "Rename" && + item.label !== "New Directory" && + item.label !== "Upload File", + ); + } + + return items; + }; + + const buildNotebookDirectoryNode = ( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + isGithubTree?: boolean, + ): TreeNode => { + return { + label: item.name, + iconSrc: undefined, + className: "notebookHeader", + isAlphaSorted: true, + isLeavesParentsSeparate: true, + onClick: () => { + if (!item.children) { + loadSubitems(item, isGithubTree); + } + }, + isSelected: () => { + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined, + data: item, + children: buildChildNodes(item, onFileClick, isGithubTree), + }; + }; const buildDataTree = (): TreeNode => { const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => { @@ -368,6 +757,11 @@ export const ResourceTree: React.FC = ({ container }: Resourc return traverse(schema); }; + const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise => { + const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item); + updateNotebookItem(updatedItem, isGithubTree); + }; + const dataRootNode = buildDataTree(); const isSampleDataEnabled = useQueryCopilot().copilotEnabled && @@ -381,16 +775,46 @@ export const ResourceTree: React.FC = ({ container }: Resourc {!isNotebookEnabled && !isSampleDataEnabled && ( )} + {isNotebookEnabled && !isSampleDataEnabled && ( + <> + + + + + + + {/* {buildGalleryCallout()} */} + + )} {!isNotebookEnabled && isSampleDataEnabled && ( <> - + + + {/* {buildGalleryCallout()} */} + + )} + {isNotebookEnabled && isSampleDataEnabled && ( + <> + + + + + + + + + + + + + {/* {buildGalleryCallout()} */} )} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 67fb54773..b22f0f916 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,21 +1,42 @@ +import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; +import GalleryIcon from "../../../images/GalleryIcon.svg"; +import DeleteIcon from "../../../images/delete.svg"; +import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; +import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; +import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; +import FileIcon from "../../../images/notebook/file-cosmos.svg"; +import PublishIcon from "../../../images/notebook/publish_content.svg"; +import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; +import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; +import { IPinnedRepo } from "../../Juno/JunoClient"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; +import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; +import * as GitHubUtils from "../../Utils/GitHubUtils"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent"; +import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { useDialog } from "../Controls/Dialog"; +import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; +import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; +import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; +import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -25,8 +46,19 @@ import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; export class ResourceTreeAdapter implements ReactAdapter { + public static readonly MyNotebooksTitle = "My Notebooks"; + public static readonly GitHubReposTitle = "GitHub repos"; + + private static readonly DataTitle = "DATA"; + private static readonly NotebooksTitle = "NOTEBOOKS"; + private static readonly PseudoDirPath = "PsuedoDir"; + public parameters: ko.Observable; + public galleryContentRoot: NotebookContentItem; + public myNotebooksContentRoot: NotebookContentItem; + public gitHubNotebooksContentRoot: NotebookContentItem; + public constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); @@ -44,9 +76,111 @@ export class ResourceTreeAdapter implements ReactAdapter { this.triggerRender(); } + private traceMyNotebookTreeInfo() { + const myNotebooksTree = this.myNotebooksContentRoot; + if (myNotebooksTree.children) { + // Count 1st generation children (tree is lazy-loaded) + const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; + myNotebooksTree.children.forEach((treeNode) => { + switch ((treeNode as NotebookContentItem).type) { + case NotebookContentItemType.File: + nodeCounts.files++; + break; + case NotebookContentItemType.Directory: + nodeCounts.directories++; + break; + case NotebookContentItemType.Notebook: + nodeCounts.notebooks++; + break; + default: + break; + } + }); + TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts }); + } + } + public renderComponent(): JSX.Element { const dataRootNode = this.buildDataTree(); - return ; + const notebooksRootNode = this.buildNotebooksTrees(); + + if (useNotebook.getState().isNotebookEnabled) { + return ( + <> + + + + + + + + + + {/* {this.galleryContentRoot && this.buildGalleryCallout()} */} + + ); + } else { + return ; + } + } + + public async initialize(): Promise { + const refreshTasks: Promise[] = []; + + this.galleryContentRoot = { + name: "Gallery", + path: "Gallery", + type: NotebookContentItemType.File, + }; + this.myNotebooksContentRoot = { + name: useNotebook.getState().notebookFolderName, + path: useNotebook.getState().notebookBasePath, + type: NotebookContentItemType.Directory, + }; + + // Only if notebook server is available we can refresh + if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) { + refreshTasks.push( + this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => { + this.triggerRender(); + this.traceMyNotebookTreeInfo(); + }), + ); + } + this.gitHubNotebooksContentRoot = { + name: ResourceTreeAdapter.GitHubReposTitle, + path: ResourceTreeAdapter.PseudoDirPath, + type: NotebookContentItemType.Directory, + }; + + return Promise.all(refreshTasks); + } + + public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { + if (this.gitHubNotebooksContentRoot) { + this.gitHubNotebooksContentRoot.children = []; + pinnedRepos?.forEach((pinnedRepo) => { + const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); + const repoTreeItem: NotebookContentItem = { + name: repoFullName, + path: ResourceTreeAdapter.PseudoDirPath, + type: NotebookContentItemType.Directory, + children: [], + }; + + pinnedRepo.branches.forEach((branch) => { + repoTreeItem.children.push({ + name: branch.name, + path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), + type: NotebookContentItemType.Directory, + }); + }); + + this.gitHubNotebooksContentRoot.children.push(repoTreeItem); + }); + + this.triggerRender(); + } } private buildDataTree(): TreeNode { @@ -370,6 +504,365 @@ export class ResourceTreeAdapter implements ReactAdapter { return traverse(schema); } + private buildNotebooksTrees(): TreeNode { + let notebooksTree: TreeNode = { + label: undefined, + isExpanded: true, + children: [], + }; + + if (this.galleryContentRoot) { + notebooksTree.children.push(this.buildGalleryNotebooksTree()); + } + + if (this.myNotebooksContentRoot) { + notebooksTree.children.push(this.buildMyNotebooksTree()); + } + + if (this.gitHubNotebooksContentRoot) { + // collapse all other notebook nodes + notebooksTree.children.forEach((node) => (node.isExpanded = false)); + notebooksTree.children.push(this.buildGitHubNotebooksTree()); + } + + return notebooksTree; + } + + private buildGalleryCallout(): JSX.Element { + if ( + LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && + LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) + ) { + return undefined; + } + + const calloutProps: ICalloutProps = { + calloutMaxWidth: 350, + ariaLabel: "New gallery", + role: "alertdialog", + gapSpace: 0, + target: ".galleryHeader", + directionalHint: DirectionalHint.leftTopEdge, + onDismiss: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + this.triggerRender(); + }, + setInitialFocus: true, + }; + + const openGalleryProps: ILinkProps = { + onClick: () => { + LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); + this.container.openGallery(); + this.triggerRender(); + }, + }; + + return ( + + + + New gallery + + + Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other + contributors. + + Open gallery + + + ); + } + + private buildGalleryNotebooksTree(): TreeNode { + return { + label: "Gallery", + iconSrc: GalleryIcon, + className: "notebookHeader galleryHeader", + onClick: () => this.container.openGallery(), + isSelected: () => { + const activeTab = useTabs.getState().activeTab; + return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; + }, + }; + } + + private buildMyNotebooksTree(): TreeNode { + const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( + this.myNotebooksContentRoot, + (item: NotebookContentItem) => { + this.container.openNotebook(item).then((hasOpened) => { + if (hasOpened) { + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); + } + }); + }, + true, + true, + ); + + myNotebooksTree.isExpanded = true; + myNotebooksTree.isAlphaSorted = true; + // Remove "Delete" menu item from context menu + myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); + return myNotebooksTree; + } + + private buildGitHubNotebooksTree(): TreeNode { + const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( + this.gitHubNotebooksContentRoot, + (item: NotebookContentItem) => { + this.container.openNotebook(item).then((hasOpened) => { + if (hasOpened) { + mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); + } + }); + }, + true, + true, + ); + + gitHubNotebooksTree.contextMenu = [ + { + label: "Manage GitHub settings", + onClick: () => + useSidePanel + .getState() + .openSidePanel( + "Manage GitHub settings", + , + ), + }, + { + label: "Disconnect from GitHub", + onClick: () => { + TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { + dataExplorerArea: Areas.Notebook, + }); + this.container.notebookManager?.gitHubOAuthService.logout(); + }, + }, + ]; + + gitHubNotebooksTree.isExpanded = true; + gitHubNotebooksTree.isAlphaSorted = true; + + return gitHubNotebooksTree; + } + + private buildChildNodes( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + createDirectoryContextMenu: boolean, + createFileContextMenu: boolean, + ): TreeNode[] { + if (!item || !item.children) { + return []; + } else { + return item.children.map((item) => { + const result = + item.type === NotebookContentItemType.Directory + ? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu) + : this.buildNotebookFileNode(item, onFileClick, createFileContextMenu); + result.timestamp = item.timestamp; + return result; + }); + } + } + + private buildNotebookFileNode( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + createFileContextMenu: boolean, + ): TreeNode { + return { + label: item.name, + iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon, + className: "notebookHeader", + onClick: () => onFileClick(item), + isSelected: () => { + const activeTab = useTabs.getState().activeTab; + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: createFileContextMenu && this.createFileContextMenu(item), + data: item, + }; + } + + private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { + let items: TreeNodeMenuItem[] = [ + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => this.container.renameNotebook(item), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined, + ); + }, + }, + { + label: "Copy to ...", + iconSrc: CopyIcon, + onClick: () => this.copyNotebook(item), + }, + { + label: "Download", + iconSrc: NotebookIcon, + onClick: () => this.container.downloadFile(item), + }, + ]; + + if (item.type === NotebookContentItemType.Notebook) { + items.push({ + label: "Publish to gallery", + iconSrc: PublishIcon, + onClick: async () => { + TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, { + source: Source.ResourceTreeMenu, + }); + + const content = await this.container.readFile(item); + if (content) { + await this.container.publishNotebook(item.name, content); + } + }, + }); + } + + // "Copy to ..." isn't needed if github locations are not available + if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + items = items.filter((item) => item.label !== "Copy to ..."); + } + + return items; + } + + private copyNotebook = async (item: NotebookContentItem) => { + const content = await this.container.readFile(item); + if (content) { + this.container.copyNotebook(item.name, content); + } + }; + + private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] { + let items: TreeNodeMenuItem[] = [ + { + label: "Refresh", + iconSrc: RefreshIcon, + onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()), + }, + { + label: "Delete", + iconSrc: DeleteIcon, + onClick: () => { + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined, + ); + }, + }, + { + label: "Rename", + iconSrc: NotebookIcon, + onClick: () => this.container.renameNotebook(item), + }, + { + label: "New Directory", + iconSrc: NewNotebookIcon, + onClick: () => this.container.onCreateDirectory(item), + }, + { + label: "Upload File", + iconSrc: NewNotebookIcon, + onClick: () => this.container.openUploadFilePanel(item), + }, + ]; + + //disallow renaming of temporary notebook workspace + if (item?.path === useNotebook.getState().notebookBasePath) { + items = items.filter((item) => item.label !== "Rename"); + } + + // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" + if (GitHubUtils.fromContentUri(item.path)) { + items = items.filter( + (item) => + item.label !== "Delete" && + item.label !== "Rename" && + item.label !== "New Directory" && + item.label !== "Upload File", + ); + } + + return items; + } + + private buildNotebookDirectoryNode( + item: NotebookContentItem, + onFileClick: (item: NotebookContentItem) => void, + createDirectoryContextMenu: boolean, + createFileContextMenu: boolean, + ): TreeNode { + return { + label: item.name, + iconSrc: undefined, + className: "notebookHeader", + isAlphaSorted: true, + isLeavesParentsSeparate: true, + onClick: () => { + if (!item.children) { + this.container.refreshContentItem(item).then(() => this.triggerRender()); + } + }, + isSelected: () => { + const activeTab = useTabs.getState().activeTab; + return ( + activeTab && + activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && + /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. + NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. + */ + (activeTab as any).notebookPath() === item.path + ); + }, + contextMenu: + createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath + ? this.createDirectoryContextMenu(item) + : undefined, + data: item, + children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), + }; + } + public triggerRender() { window.requestAnimationFrame(() => this.parameters(Date.now())); } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 1314b2b3c..f11b6df5c 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -245,6 +245,7 @@ export function downloadItem( }, "Cancel", undefined, + container.getDownloadModalConent(name), ); } export async function downloadNotebookItem( @@ -277,6 +278,7 @@ export async function downloadNotebookItem( metadata.untrusted = true; } + await container.importAndOpenContent(data.name, JSON.stringify(notebook)); logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`); const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id); From 6ebc48ad28a280a96152aaab968b990ff8b5bef0 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Thu, 2 May 2024 07:14:31 -0700 Subject: [PATCH 32/67] Remove some Notebooks code (#1832) * Remove onNewNotebookClicked, openUploadFilePanel functions and UploadFilePane. * Remove resetNotebookWorkspace function. * Remove Notebooks related resource tree node generation. * Fix test snapshots. --- .../SettingsComponent.test.tsx.snap | 4 - src/Explorer/Explorer.tsx | 220 ------------------ .../GitHubReposPanel.test.tsx.snap | 1 - .../StringInputPane.test.tsx.snap | 1 - .../Panes/UploadFilePane/UploadFilePane.tsx | 91 -------- .../QueryCopilotTab.test.tsx.snap | 1 - src/Explorer/SplashScreen/SplashScreen.tsx | 9 - src/Explorer/Tree/ResourceTree.tsx | 166 ------------- src/Explorer/Tree/ResourceTreeAdapter.tsx | 183 +-------------- 9 files changed, 1 insertion(+), 675 deletions(-) delete mode 100644 src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index ab7abac11..65eb765e5 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -30,7 +30,6 @@ exports[`SettingsComponent renders 1`] = ` "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -108,7 +107,6 @@ exports[`SettingsComponent renders 1`] = ` "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -225,7 +223,6 @@ exports[`SettingsComponent renders 1`] = ` "conflictResolutionPolicy": [Function], "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], @@ -272,7 +269,6 @@ exports[`SettingsComponent renders 1`] = ` explorer={ Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 4af478475..fe648918b 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -38,7 +38,6 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; -import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; @@ -56,7 +55,6 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; @@ -510,104 +508,6 @@ export default class Explorer { .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); } - public resetNotebookWorkspace(): void { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { - handleError( - "Attempt to reset notebook workspace, but notebook is not enabled", - "Explorer/resetNotebookWorkspace", - ); - return; - } - const dialogContent = useNotebook.getState().isPhoenixNotebooks - ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" - : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; - - const resetConfirmationDialogProps: DialogProps = { - isModal: true, - title: "Reset Workspace", - subText: dialogContent, - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._resetNotebookWorkspace, - onSecondaryButtonClick: () => useDialog.getState().closeDialog(), - }; - useDialog.getState().openDialog(resetConfirmationDialogProps); - } - - private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - return false; - } - try { - const { value: workspaces } = await listByDatabaseAccount( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - ); - return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default"); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); - return false; - } - } - - private _resetNotebookWorkspace = async () => { - useDialog.getState().closeDialog(); - const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); - let connectionStatus: ContainerConnectionInfo; - try { - const notebookServerInfo = useNotebook.getState().notebookServerInfo; - if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { - const error = "No server endpoint detected"; - Logger.logError(error, "NotebookContainerClient/resetWorkspace"); - logConsoleError(error); - return; - } - TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - if (useNotebook.getState().isPhoenixNotebooks) { - useTabs.getState().closeAllNotebookTabs(true); - connectionStatus = { - status: ConnectionStatusType.Connecting, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); - } - const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace(); - if (connectionInfo?.status !== HttpStatusCodes.OK) { - throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`); - } - if (!connectionInfo?.data?.phoenixServiceUrl) { - throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`); - } - if (useNotebook.getState().isPhoenixNotebooks) { - await this.setNotebookInfo(true, connectionInfo, connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - logConsoleInfo("Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - }); - } catch (error) { - logConsoleError(`Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, { - dataExplorerArea: Areas.Notebook, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }); - if (useNotebook.getState().isPhoenixNotebooks) { - connectionStatus = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().resetContainerConnection(connectionStatus); - useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); - } - throw error; - } finally { - clearInProgressMessage(); - } - }; - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[], databases: ViewModels.Database[], @@ -1010,92 +910,6 @@ export default class Explorer { ); } - /** - * This creates a new notebook file, then opens the notebook - */ - public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise { - if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create new notebook, but notebook is not enabled"; - handleError(error, "Explorer/onNewNotebookClicked"); - throw new Error(error); - } - if (useNotebook.getState().isPhoenixNotebooks) { - if (isGithubTree) { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } else { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookModalTitle, - undefined, - "Create", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.createNewNoteBook(parent, isGithubTree); - } - } - - private getNewNoteWarningText(): JSX.Element { - return ( - <> -

{Notebook.newNotebookModalContent1}

-
-

- {Notebook.newNotebookModalContent2} - - {Notebook.learnMore} - -

- - ); - } - - private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { - const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { - dataExplorerArea: Constants.Areas.Notebook, - }); - - this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent, isGithubTree) - .then((newFile: NotebookContentItem) => { - logConsoleInfo(`Successfully created: ${newFile.name}`); - TelemetryProcessor.traceSuccess( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - }, - startKey, - ); - return this.openNotebook(newFile); - }) - .then(() => this.resourceTree.triggerRender()) - .catch((error) => { - const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; - logConsoleError(errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateNewNotebook, - { - dataExplorerArea: Constants.Areas.Notebook, - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }) - .finally(clearInProgressMessage); - } - // TODO: Delete this function when ResourceTreeAdapter is removed. public async refreshContentItem(item: NotebookContentItem): Promise { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { @@ -1130,10 +944,6 @@ export default class Explorer { let title: string; switch (kind) { - case ViewModels.TerminalKind.Default: - title = "Terminal"; - break; - case ViewModels.TerminalKind.Mongo: title = "Mongo Shell"; break; @@ -1287,36 +1097,6 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public openUploadFilePanel(parent?: NotebookContentItem): void { - if (useNotebook.getState().isPhoenixNotebooks) { - useDialog.getState().showOkCancelModalDialog( - Notebook.newNotebookUploadModalTitle, - undefined, - "Upload", - async () => { - await this.allocateContainer(PoolIdType.DefaultPoolId); - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - }, - "Cancel", - undefined, - this.getNewNoteWarningText(), - ); - } else { - parent = parent || this.resourceTree.myNotebooksContentRoot; - this.uploadFilePanel(parent); - } - } - - private uploadFilePanel(parent?: NotebookContentItem): void { - useSidePanel - .getState() - .openSidePanel( - "Upload file to notebook server", - this.uploadFile(name, content, parent)} />, - ); - } - public getDownloadModalConent(fileName: string): JSX.Element { if (useNotebook.getState().isPhoenixNotebooks) { return ( diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 4a3a8942e..19f98d5c6 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -18,7 +18,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` Object { "container": Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 8054abe19..3e8f7c92d 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -8,7 +8,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` explorer={ Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx b/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx deleted file mode 100644 index d9b1d9792..000000000 --- a/src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Upload } from "Common/Upload/Upload"; -import { useSidePanel } from "hooks/useSidePanel"; -import React, { ChangeEvent, FunctionComponent, useState } from "react"; -import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils"; -import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; -import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; - -export interface UploadFilePanelProps { - uploadFile: (name: string, content: string) => Promise; -} - -export const UploadFilePane: FunctionComponent = ({ uploadFile }: UploadFilePanelProps) => { - const closeSidePanel = useSidePanel((state) => state.closeSidePanel); - const extensions: string = undefined; //ex. ".ipynb" - const errorMessage = "Could not upload file"; - const inProgressMessage = "Uploading file to notebook server"; - const successMessage = "Successfully uploaded file to notebook server"; - - const [files, setFiles] = useState(); - const [formErrors, setFormErrors] = useState(""); - const [isExecuting, setIsExecuting] = useState(false); - - const submit = () => { - setFormErrors(""); - if (!files || files.length === 0) { - setFormErrors("No file specified. Please input a file."); - logConsoleError(`${errorMessage} -- No file specified. Please input a file.`); - return; - } - - const file: File = files.item(0); - - const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`); - - setIsExecuting(true); - - onSubmit(files.item(0)) - .then( - () => { - logConsoleInfo(`${successMessage} ${file.name}`); - closeSidePanel(); - }, - (error: string) => { - setFormErrors(errorMessage); - logConsoleError(`${errorMessage} ${file.name}: ${error}`); - }, - ) - .finally(() => { - setIsExecuting(false); - clearMessage(); - }); - }; - - const updateSelectedFiles = (event: ChangeEvent): void => { - setFiles(event.target.files); - }; - - const onSubmit = async (file: File): Promise => { - const readFileAsText = (inputFile: File): Promise => { - const reader = new FileReader(); - return new Promise((resolve, reject) => { - reader.onerror = () => { - reader.abort(); - reject(`Problem parsing file: ${inputFile}`); - }; - reader.onload = () => { - resolve(reader.result as string); - }; - reader.readAsText(inputFile); - }); - }; - - const fileContent = await readFileAsText(file); - return uploadFile(file.name, fileContent); - }; - - const props: RightPaneFormProps = { - formError: formErrors, - isExecuting: isExecuting, - submitButtonText: "Upload", - onSubmit: submit, - }; - - return ( - -
- -
-
- ); -}; diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 26b52ff90..bb942bf55 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -23,7 +23,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = explorer={ Explorer { "_isInitializingNotebooks": false, - "_resetNotebookWorkspace": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index f4ebb9cd0..0374edd23 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -25,7 +25,6 @@ import * as React from "react"; import ConnectIcon from "../../../images/Connect_color.svg"; import ContainersIcon from "../../../images/Containers.svg"; import LinkIcon from "../../../images/Link_blue.svg"; -import NotebookColorIcon from "../../../images/Notebooks.svg"; import PowerShellIcon from "../../../images/PowerShell.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; @@ -410,14 +409,6 @@ export class SplashScreen extends React.Component { }, }; heroes.push(launchQuickstartBtn); - } else if (useNotebook.getState().isPhoenixNotebooks) { - const newNotebookBtn = { - iconSrc: NotebookColorIcon, - title: "New notebook", - description: "Visualize your data stored in Azure Cosmos DB", - onClick: () => this.container.onNewNotebookClicked(), - }; - heroes.push(newNotebookBtn); } heroes.push(this.getShellCard()); diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b5f759534..a14c202e8 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,11 +1,9 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { getItemName } from "Utils/APITypeUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as React from "react"; import shallow from "zustand/shallow"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; import DeleteIcon from "../../../images/delete.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; @@ -14,17 +12,14 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; -import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; @@ -36,7 +31,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -75,152 +69,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin"); const pseudoDirPath = "PsuedoDir"; - const buildGalleryCallout = (): JSX.Element => { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - container.openGallery(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - }; - - const buildNotebooksTree = (): TreeNode => { - const notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (!useNotebook.getState().isPhoenixNotebooks) { - notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); - } else { - if (galleryContentRoot) { - notebooksTree.children.push(buildGalleryNotebooksTree()); - } - - if ( - myNotebooksContentRoot && - useNotebook.getState().isPhoenixNotebooks && - useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected - ) { - notebooksTree.children.push(buildMyNotebooksTree()); - } - if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(buildGitHubNotebooksTree(true)); - } - } - return notebooksTree; - }; - - const buildNotebooksTemporarilyDownTree = (): TreeNode => { - return { - label: Notebook.temporarilyDownMsg, - className: "clickDisabled", - }; - }; - - const buildGalleryNotebooksTree = (): TreeNode => { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => container.openGallery(), - isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery, - }; - }; - - const buildMyNotebooksTree = (): TreeNode => { - const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( - myNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - }; - - const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { - const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( - gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - container.openNotebook(item); - }, - true, - ); - const manageGitContextMenu: TreeNodeMenuItem[] = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - gitHubNotebooksTree.contextMenu = manageGitContextMenu; - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - }; - const buildChildNodes = ( item: NotebookContentItem, onFileClick: (item: NotebookContentItem) => void, @@ -373,11 +221,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc iconSrc: NewNotebookIcon, onClick: () => container.onCreateDirectory(item, isGithubTree), }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => container.openUploadFilePanel(item), - }, ]; //disallow renaming of temporary notebook workspace @@ -782,8 +625,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - {/* {buildGalleryCallout()} */} )} {!isNotebookEnabled && isSampleDataEnabled && ( @@ -796,8 +637,6 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - {/* {buildGalleryCallout()} */} )} {isNotebookEnabled && isSampleDataEnabled && ( @@ -809,12 +648,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc - - - - - {/* {buildGalleryCallout()} */} )} diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index b22f0f916..2fd8d6bd1 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,9 +1,7 @@ -import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; -import GalleryIcon from "../../../images/GalleryIcon.svg"; import DeleteIcon from "../../../images/delete.svg"; import CopyIcon from "../../../images/notebook/Notebook-copy.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; @@ -13,21 +11,17 @@ import PublishIcon from "../../../images/notebook/publish_content.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg"; import CollectionIcon from "../../../images/tree-collection.svg"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { Areas } from "../../Common/Constants"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { IPinnedRepo } from "../../Juno/JunoClient"; -import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; -import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; @@ -36,7 +30,6 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; @@ -102,26 +95,7 @@ export class ResourceTreeAdapter implements ReactAdapter { public renderComponent(): JSX.Element { const dataRootNode = this.buildDataTree(); - const notebooksRootNode = this.buildNotebooksTrees(); - - if (useNotebook.getState().isNotebookEnabled) { - return ( - <> - - - - - - - - - - {/* {this.galleryContentRoot && this.buildGalleryCallout()} */} - - ); - } else { - return ; - } + return ; } public async initialize(): Promise { @@ -504,156 +478,6 @@ export class ResourceTreeAdapter implements ReactAdapter { return traverse(schema); } - private buildNotebooksTrees(): TreeNode { - let notebooksTree: TreeNode = { - label: undefined, - isExpanded: true, - children: [], - }; - - if (this.galleryContentRoot) { - notebooksTree.children.push(this.buildGalleryNotebooksTree()); - } - - if (this.myNotebooksContentRoot) { - notebooksTree.children.push(this.buildMyNotebooksTree()); - } - - if (this.gitHubNotebooksContentRoot) { - // collapse all other notebook nodes - notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(this.buildGitHubNotebooksTree()); - } - - return notebooksTree; - } - - private buildGalleryCallout(): JSX.Element { - if ( - LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) && - LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed) - ) { - return undefined; - } - - const calloutProps: ICalloutProps = { - calloutMaxWidth: 350, - ariaLabel: "New gallery", - role: "alertdialog", - gapSpace: 0, - target: ".galleryHeader", - directionalHint: DirectionalHint.leftTopEdge, - onDismiss: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.triggerRender(); - }, - setInitialFocus: true, - }; - - const openGalleryProps: ILinkProps = { - onClick: () => { - LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true); - this.container.openGallery(); - this.triggerRender(); - }, - }; - - return ( - - - - New gallery - - - Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other - contributors. - - Open gallery - - - ); - } - - private buildGalleryNotebooksTree(): TreeNode { - return { - label: "Gallery", - iconSrc: GalleryIcon, - className: "notebookHeader galleryHeader", - onClick: () => this.container.openGallery(), - isSelected: () => { - const activeTab = useTabs.getState().activeTab; - return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery; - }, - }; - } - - private buildMyNotebooksTree(): TreeNode { - const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.myNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - myNotebooksTree.isExpanded = true; - myNotebooksTree.isAlphaSorted = true; - // Remove "Delete" menu item from context menu - myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete"); - return myNotebooksTree; - } - - private buildGitHubNotebooksTree(): TreeNode { - const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.gitHubNotebooksContentRoot, - (item: NotebookContentItem) => { - this.container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { - mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); - } - }); - }, - true, - true, - ); - - gitHubNotebooksTree.contextMenu = [ - { - label: "Manage GitHub settings", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ), - }, - { - label: "Disconnect from GitHub", - onClick: () => { - TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - this.container.notebookManager?.gitHubOAuthService.logout(); - }, - }, - ]; - - gitHubNotebooksTree.isExpanded = true; - gitHubNotebooksTree.isAlphaSorted = true; - - return gitHubNotebooksTree; - } - private buildChildNodes( item: NotebookContentItem, onFileClick: (item: NotebookContentItem) => void, @@ -800,11 +624,6 @@ export class ResourceTreeAdapter implements ReactAdapter { iconSrc: NewNotebookIcon, onClick: () => this.container.onCreateDirectory(item), }, - { - label: "Upload File", - iconSrc: NewNotebookIcon, - onClick: () => this.container.openUploadFilePanel(item), - }, ]; //disallow renaming of temporary notebook workspace From 5c3f18f5f8dcb47884b063c7411d65e2ed304854 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 7 May 2024 12:30:46 -0700 Subject: [PATCH 33/67] add link to keyboard shortcuts doc to home tab (#1836) --- src/Explorer/SplashScreen/SplashScreen.tsx | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 0374edd23..0ed9351ae 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -680,11 +680,20 @@ export class SplashScreen extends React.Component { title: "Learn the Fundamentals", description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.", }; - let items: item[]; + + const commonItems: item[] = [ + { + link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts", + title: "Data Explorer keyboard shortcuts", + description: "Learn keyboard shortcuts to navigate Data Explorer.", + }, + ]; + + let apiItems: item[]; switch (userContext.apiType) { case "SQL": case "Postgres": - items = [ + apiItems = [ { link: "https://aka.ms/msl-sdk-connect", title: "Get Started using an SDK", @@ -699,7 +708,7 @@ export class SplashScreen extends React.Component { ]; break; case "Mongo": - items = [ + apiItems = [ { link: "https://aka.ms/mongonodejs", title: "Build an app with Node.js", @@ -714,7 +723,7 @@ export class SplashScreen extends React.Component { ]; break; case "Cassandra": - items = [ + apiItems = [ { link: "https://aka.ms/cassandracontainer", title: "Create a Container", @@ -729,7 +738,7 @@ export class SplashScreen extends React.Component { ]; break; case "Gremlin": - items = [ + apiItems = [ { link: "https://aka.ms/graphquickstart", title: "Get Started ", @@ -744,7 +753,7 @@ export class SplashScreen extends React.Component { ]; break; case "Tables": - items = [ + apiItems = [ { link: "https://aka.ms/tabledotnet", title: "Build a .NET App", @@ -761,6 +770,9 @@ export class SplashScreen extends React.Component { default: break; } + + const items = [...commonItems, ...apiItems]; + return ( {items.map((item, i) => ( From 14e5efcebf5cd56d04a6627184a437b750c644ac Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Thu, 9 May 2024 13:42:59 -0400 Subject: [PATCH 34/67] Point Mongo requests to old backend (#1838) * point mongo requests to old backend * point mongo requests to old backend --------- Co-authored-by: Asier Isayas --- src/ConfigContext.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 061f25286..ed8eaab0e 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -102,13 +102,13 @@ let configContext: Readonly = { PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, NEW_MONGO_APIS: [ - "resourcelist", - "queryDocuments", - "createDocument", - "readDocument", - "updateDocument", - "deleteDocument", - "createCollectionWithProxy", + // "resourcelist", + // "queryDocuments", + // "createDocument", + // "readDocument", + // "updateDocument", + // "deleteDocument", + // "createCollectionWithProxy", "legacyMongoShell", ], MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, From f56e5e64b90c0790a00b372fd621e4e9ffb0157f Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Tue, 14 May 2024 09:56:16 +0530 Subject: [PATCH 35/67] =?UTF-8?q?[accessibility-3102916]:[Keyboard=20Navig?= =?UTF-8?q?ation=20-=20Azure=20CosmosDB=20-=20Data=20=E2=80=A6=20(#1834)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [accessibility-3102916]:[Keyboard Navigation - Azure CosmosDB - Data Explorer]: Keyboard focus is moving to non-interactive control after checkbox control of Advanced button. * Updated Snap. --------- Co-authored-by: Satyapriya Bai --- src/Explorer/Panes/AddCollectionPanel.tsx | 7 +++---- .../Panes/__snapshots__/AddCollectionPanel.test.tsx.snap | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 2a99a56a7..dee78d7bf 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -924,10 +924,9 @@ export class AddCollectionPanel extends React.Component - To ensure compatibility with - older SDKs, the created container will use a legacy partitioning scheme that supports partition - key values of size only up to 101 bytes. If this is enabled, you will not be able to use - hierarchical partition keys.{" "} + To ensure compatibility with older SDKs, the + created container will use a legacy partitioning scheme that supports partition key values of size + only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "} Learn more diff --git a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap index 79485d550..d2fd03913 100644 --- a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap @@ -466,7 +466,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = ` To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys. From 9e9d270b65ed43d1772044368ac30dae00950599 Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Tue, 14 May 2024 09:56:53 +0530 Subject: [PATCH 36/67] =?UTF-8?q?[accessibility-3102877]:[Programmatic=20A?= =?UTF-8?q?ccess=20-=20Azure=20CosmosDB=20=E2=80=93=20Data=20Explorer]:=20?= =?UTF-8?q?Ensures=20every=20ARIA=20input=20field=20has=20an=20accessible?= =?UTF-8?q?=20name.=20(#1835)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Satyapriya Bai --- src/Explorer/Panes/AddCollectionPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index dee78d7bf..8c1cfef5b 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -382,6 +382,7 @@ export class AddCollectionPanel extends React.Component Date: Tue, 14 May 2024 09:57:50 +0530 Subject: [PATCH 37/67] Screen Reader does not announce status message after invoking 'Add Row' control under 'Add Table Row' pane. (#1837) * [accessibility-3100026]: [Screen Reader - Azure Cosmos DB - Add Table Row]: Screen Reader does not announce status message after invoking 'Add Row' control under 'Add Table Row' pane. * Fixed format. * Snap update. --------- Co-authored-by: Satyapriya Bai --- less/Common/Constants.less | 7 +++++++ .../DeleteCollectionConfirmationPane.test.tsx.snap | 5 +++++ .../__snapshots__/ExecuteSprocParamsPane.test.tsx.snap | 5 +++++ src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx | 6 ++++++ .../__snapshots__/RightPaneForm.test.tsx.snap | 5 +++++ .../__snapshots__/StringInputPane.test.tsx.snap | 5 +++++ .../__snapshots__/TableQuerySelectPanel.test.tsx.snap | 5 +++++ .../Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap | 5 +++++ .../__snapshots__/EditTableEntityPanel.test.tsx.snap | 5 +++++ .../DeleteDatabaseConfirmationPanel.test.tsx.snap | 5 +++++ src/Explorer/Tables/Constants.ts | 5 +++++ 11 files changed, 58 insertions(+) diff --git a/less/Common/Constants.less b/less/Common/Constants.less index 946426d37..5b80d67f8 100644 --- a/less/Common/Constants.less +++ b/less/Common/Constants.less @@ -335,4 +335,11 @@ width: 0; height: 0; border-color: @InfoPointerColor transparent; +} +/********************************************************************************************************* + Screen Reader Only +**********************************************************************************************************/ +.screenReaderOnly { + position: absolute; + left: -9999px; } \ No newline at end of file diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index e63cb45bd..8add09097 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -2140,6 +2140,11 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
+ `; diff --git a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap index 6e0cbbeab..6c5cb5dfb 100644 --- a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap +++ b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap @@ -7073,6 +7073,11 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
+ `; diff --git a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx index 27ba8e7f6..575403ccf 100644 --- a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx +++ b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties, FunctionComponent, ReactNode } from "react"; import { PanelFooterComponent } from "../PanelFooterComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelLoadingScreen } from "../PanelLoadingScreen"; +import { labelToLoadingItemName } from "Explorer/Tables/Constants"; export interface RightPaneFormProps { formError: string; @@ -27,6 +28,10 @@ export const RightPaneForm: FunctionComponent = ({ const handleOnSubmit = (event: React.FormEvent) => { event.preventDefault(); onSubmit(); + const screenReaderStatusElement = document.getElementById("screenReaderStatus"); + if (screenReaderStatusElement) { + screenReaderStatusElement.innerHTML = labelToLoadingItemName[submitButtonText] || "Loading"; + } }; return ( @@ -42,6 +47,7 @@ export const RightPaneForm: FunctionComponent = ({ /> )} + {isExecuting && } ); diff --git a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap index bcfa8a9e6..7691f6ed4 100644 --- a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap +++ b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap @@ -1782,5 +1782,10 @@ exports[`Right Pane Form should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 3e8f7c92d..e8553fa4e 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -2449,6 +2449,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` + `; diff --git a/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap b/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap index ca5c6c926..4a913958d 100644 --- a/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap @@ -3019,6 +3019,11 @@ exports[`Table query select Panel should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap index 80d494808..f2ee599f7 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap @@ -2130,6 +2130,11 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap index da5c0c722..648f797f7 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap @@ -2136,6 +2136,11 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` + `; diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 4ac4a9e20..6e32d62c0 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -2810,6 +2810,11 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` + `; diff --git a/src/Explorer/Tables/Constants.ts b/src/Explorer/Tables/Constants.ts index 76c050f5d..58d5b8e87 100644 --- a/src/Explorer/Tables/Constants.ts +++ b/src/Explorer/Tables/Constants.ts @@ -225,3 +225,8 @@ export const InputType = { DateTime: "datetime-local", Number: "number", }; + +export const labelToLoadingItemName: Record = { + "Add Row": "Adding row to table", + "Add Entity": "Adding entity to table", +}; From ff4bc78d6cf5c7047f91ee1a13e03d512aec27aa Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Fri, 17 May 2024 06:57:35 -0700 Subject: [PATCH 38/67] Remove preview label from Computed Properties. (#1842) --- src/Explorer/Controls/Settings/SettingsUtils.tsx | 2 +- .../Settings/__snapshots__/SettingsComponent.test.tsx.snap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 869ae323a..24ef14681 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -151,7 +151,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { case SettingsV2TabTypes.PartitionKeyTab: return "Partition Keys (preview)"; case SettingsV2TabTypes.ComputedPropertiesTab: - return "Computed Properties (preview)"; + return "Computed Properties"; default: throw new Error(`Unknown tab ${tab}`); } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 65eb765e5..b536bed8a 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -296,7 +296,7 @@ exports[`SettingsComponent renders 1`] = ` /> Date: Fri, 17 May 2024 12:19:23 -0500 Subject: [PATCH 39/67] add capacityMode (#1826) * add capacityMode * add check for capacityMode for serverless --- src/Common/Constants.ts | 5 +++++ src/Contracts/DataModels.ts | 3 ++- src/Utils/CapabilityUtils.ts | 8 +++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 69bd5ed49..0658d28c5 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -90,6 +90,11 @@ export class CapabilityNames { public static readonly EnableServerless: string = "EnableServerless"; } +export enum CapacityMode { + Provisioned = "Provisioned", + Serverless = "Serverless", +} + // flight names returned from the portal are always lowercase export class Flights { public static readonly SettingsV2 = "settingsv2"; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 3c8a5175d..e85f56964 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -1,4 +1,4 @@ -import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants"; +import { CapacityMode, ConnectionStatusType, ContainerStatusType } from "../Common/Constants"; export interface ArmEntity { id: string; @@ -35,6 +35,7 @@ export interface DatabaseAccountExtendedProperties { ipRules?: IpRule[]; privateEndpointConnections?: unknown[]; capacity?: { totalThroughputLimit: number }; + capacityMode?: CapacityMode; locations?: DatabaseAccountResponseLocation[]; postgresqlEndpoint?: string; publicNetworkAccess?: string; diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index fc2de8149..216eff13d 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -9,4 +9,10 @@ export const isCapabilityEnabled = (capabilityName: string): boolean => { return false; }; -export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless); +export const isServerlessAccount = (): boolean => { + const { databaseAccount } = userContext; + return ( + databaseAccount?.properties?.capacityMode === Constants.CapacityMode.Serverless || + isCapabilityEnabled(Constants.CapabilityNames.EnableServerless) + ); +}; From ceeead8458ceb85e0ee998ad545f456271a6f2eb Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Mon, 20 May 2024 13:30:30 -0500 Subject: [PATCH 40/67] Vector search for NoSQL accounts (#1843) * Add container vector policy and indexing policy support * Add vector search capability * hide vector settings for shared throughput DB * update package-lock * fix pipeline * remove comments * Address comments * Address comments --- package-lock.json | 8 +- package.json | 2 +- src/Common/Constants.ts | 1 + .../__snapshots__/queryDocuments.test.ts.snap | 2 + src/Common/dataAccess/createCollection.ts | 5 +- src/Common/dataAccess/queryDocuments.ts | 2 + src/Contracts/DataModels.ts | 23 ++- .../CollapsibleSectionComponent.tsx | 6 +- src/Explorer/Controls/Editor/EditorReact.tsx | 11 +- .../Controls/Settings/SettingsComponent.less | 21 ++- .../Controls/Settings/SettingsComponent.tsx | 19 +++ .../ComputedPropertiesComponent.tsx | 2 +- .../ContainerVectorPolicyComponent.tsx | 30 ++++ .../IndexingPolicyComponent.tsx | 8 +- .../ComputedPropertiesComponent.test.tsx.snap | 2 +- .../IndexingPolicyComponent.test.tsx.snap | 2 +- .../Controls/Settings/SettingsUtils.tsx | 3 + .../SettingsComponent.test.tsx.snap | 1 + src/Explorer/Panes/AddCollectionPanel.tsx | 146 +++++++++++++++++- src/Explorer/Panes/PanelComponent.less | 7 + .../AddCollectionPanel.test.tsx.snap | 5 +- src/Utils/CapabilityUtils.ts | 4 + .../generatedClients/cosmos/sqlResources.ts | 2 +- .../arm/generatedClients/cosmos/types.ts | 21 +++ 24 files changed, 297 insertions(+), 36 deletions(-) create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx diff --git a/package-lock.json b/package-lock.json index 8dcfe5d50..26c513d6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.1-beta.2", + "@azure/cosmos": "4.0.1-beta.3", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", @@ -362,9 +362,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/cosmos": { - "version": "4.0.1-beta.2", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.2.tgz", - "integrity": "sha512-iuqg/QwLQlxgRi4pnXU8JUYv+f24wkRvJ9ZZI4/sYk+DxSgkuQ194Cc2IpckpeO8z7ZpcBkVQFa82wcZVVZ8Zg==", + "version": "4.0.1-beta.3", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.3.tgz", + "integrity": "sha512-CpRGt+S5jnvtGUi4TmlS79YvxpbNc8/5/QHgIvvQ9D2ZFUqO0MjbMCU3lVZV2NAJT02BsbLfRAFe+FPn8nMRQw==", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", diff --git a/package.json b/package.json index 3a2f8b742..e1d544d1d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.0.1-beta.2", + "@azure/cosmos": "4.0.1-beta.3", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "1.5.2", "@azure/ms-rest-nodeauth": "3.1.1", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 0658d28c5..d56f5087a 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -88,6 +88,7 @@ export class CapabilityNames { public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableServerless: string = "EnableServerless"; + public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch"; } export enum CapacityMode { diff --git a/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap index 66e740abf..ec64a72b4 100644 --- a/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap +++ b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap @@ -2,6 +2,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] = ` Object { + "disableNonStreamingOrderByQuery": true, "enableScanInQuery": true, "forceQueryPlan": true, "maxDegreeOfParallelism": 0, @@ -12,6 +13,7 @@ Object { exports[`getCommonQueryOptions reads from localStorage 1`] = ` Object { + "disableNonStreamingOrderByQuery": true, "enableScanInQuery": true, "forceQueryPlan": true, "maxDegreeOfParallelism": 17, diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 73ccff3f4..1d9f41844 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -6,13 +6,13 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getCollectionName } from "../../Utils/APITypeUtils"; +import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; 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"; import { handleError } from "../ErrorHandlingUtils"; import { createMongoCollectionWithProxy } from "../MongoProxyClient"; @@ -96,6 +96,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr if (params.uniqueKeyPolicy) { resource.uniqueKeyPolicy = params.uniqueKeyPolicy; } + if (params.vectorEmbeddingPolicy) { + resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy; + } const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = { properties: { diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts index 0b8ebd29d..223fe987d 100644 --- a/src/Common/dataAccess/queryDocuments.ts +++ b/src/Common/dataAccess/queryDocuments.ts @@ -1,4 +1,5 @@ import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Queries } from "../Constants"; import { client } from "../CosmosClient"; @@ -26,5 +27,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => { (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || Queries.itemsPerPage; options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); + options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled(); return options; }; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index e85f56964..3f7e3a7db 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -158,6 +158,7 @@ export interface Collection extends Resource { changeFeedPolicy?: ChangeFeedPolicy; analyticalStorageTtl?: number; geospatialConfig?: GeospatialConfig; + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; schema?: ISchema; requestSchema?: () => void; computedProperties?: ComputedProperties; @@ -195,8 +196,14 @@ export interface IndexingPolicy { indexingMode: "consistent" | "lazy" | "none"; includedPaths: any; excludedPaths: any; - compositeIndexes?: any; - spatialIndexes?: any; + compositeIndexes?: any[]; + spatialIndexes?: any[]; + vectorIndexes?: VectorIndex[]; +} + +export interface VectorIndex { + path: string; + type: "flat" | "diskANN" | "quantizedFlat"; } export interface ComputedProperty { @@ -334,6 +341,18 @@ export interface CreateCollectionParams { partitionKey?: PartitionKey; uniqueKeyPolicy?: UniqueKeyPolicy; createMongoWildcardIndex?: boolean; + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; +} + +export interface VectorEmbeddingPolicy { + vectorEmbeddings: VectorEmbedding[]; +} + +export interface VectorEmbedding { + dataType: "float16" | "float32" | "uint8" | "int8"; + dimensions: number; + distanceFunction: "euclidean" | "cosine" | "dotproduct"; + path: string; } export interface ReadDatabaseOfferParams { diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx index d943f7cf0..207642d84 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx @@ -26,8 +26,8 @@ export class CollapsibleSectionComponent extends React.Component { @@ -75,9 +78,11 @@ export class EditorReact extends React.Component - {!this.state.showEditor && } + {!this.state.showEditor && ( + + )}
this.setRef(elt)} /> @@ -119,7 +124,7 @@ export class EditorReact extends React.Component, }); + if (this.isVectorSearchEnabled) { + tabs.push({ + tab: SettingsV2TabTypes.ContainerVectorPolicyTab, + content: , + }); + } + if (this.shouldShowIndexingPolicyEditor) { tabs.push({ tab: SettingsV2TabTypes.IndexingPolicyTab, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx index ad3b12fec..c8650b988 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx @@ -121,7 +121,7 @@ export class ComputedPropertiesComponent extends React.Component<   about how to define computed properties and how to use them. -
+
); } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx new file mode 100644 index 000000000..23ccdc84c --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx @@ -0,0 +1,30 @@ +import { Stack } from "@fluentui/react"; +import { VectorEmbeddingPolicy } from "Contracts/DataModels"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; +import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils"; +import React from "react"; + +export interface ContainerVectorPolicyComponentProps { + vectorEmbeddingPolicy: VectorEmbeddingPolicy; +} + +export const ContainerVectorPolicyComponent: React.FC = ({ + vectorEmbeddingPolicy, +}) => { + return ( + + + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index 4d6ca765f..bc5de93a4 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -16,6 +16,7 @@ export interface IndexingPolicyComponentProps { logIndexingPolicySuccessMessage: () => void; indexTransformationProgress: number; refreshIndexTransformationProgress: () => Promise; + isVectorSearchEnabled?: boolean; onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void; } @@ -119,10 +120,15 @@ export class IndexingPolicyComponent extends React.Component< indexTransformationProgress={this.props.indexTransformationProgress} refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} /> + {this.props.isVectorSearchEnabled && ( + + Container vector policies and vector indexes are not modifiable after container creation + + )} {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {unsavedEditorWarningMessage("indexPolicy")} )} -
+
); } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap index 86a9bda74..2f67d7d70 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ComputedPropertiesComponent.test.tsx.snap @@ -29,7 +29,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `   about how to define computed properties and how to use them.
diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap index 1f66324f5..93516dd7e 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/IndexingPolicyComponent.test.tsx.snap @@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = ` refreshIndexTransformationProgress={[Function]} />
diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 24ef14681..cff7d1f74 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -47,6 +47,7 @@ export enum SettingsV2TabTypes { IndexingPolicyTab, PartitionKeyTab, ComputedPropertiesTab, + ContainerVectorPolicyTab, } export interface IsComponentDirtyResult { @@ -152,6 +153,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { return "Partition Keys (preview)"; case SettingsV2TabTypes.ComputedPropertiesTab: return "Computed Properties"; + case SettingsV2TabTypes.ContainerVectorPolicyTab: + return "Container Vector Policy (preview)"; default: throw new Error(`Unknown tab ${tab}`); } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index b536bed8a..561368bdd 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -196,6 +196,7 @@ exports[`SettingsComponent renders 1`] = ` "indexingMode": "consistent", } } + isVectorSearchEnabled={false} logIndexingPolicySuccessMessage={[Function]} onIndexingPolicyContentChange={[Function]} onIndexingPolicyDirtyChange={[Function]} diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 8c1cfef5b..7bb0f553d 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -21,6 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { configContext, Platform } from "ConfigContext"; import * as DataModels from "Contracts/DataModels"; import { SubscriptionType } from "Contracts/SubscriptionType"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { useSidePanel } from "hooks/useSidePanel"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import React from "react"; @@ -29,7 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import { userContext } from "UserContext"; import { getCollectionName } from "Utils/APITypeUtils"; -import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils"; +import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { getUpsellMessage } from "Utils/PricingUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; @@ -81,6 +82,26 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = { excludedPaths: [], }; +const DefaultDatabaseVectorIndex: DataModels.IndexingPolicy = { + indexingMode: "consistent", + automatic: true, + includedPaths: [ + { + path: "/*", + }, + ], + excludedPaths: [ + { + path: '/"_etag"/?', + }, + ], + vectorIndexes: [], +}; + +export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = { + vectorEmbeddings: [], +}; + export interface AddCollectionPanelState { createNewDatabase: boolean; newDatabaseId: string; @@ -101,6 +122,8 @@ export interface AddCollectionPanelState { isExecuting: boolean; isThroughputCapExceeded: boolean; teachingBubbleStep: number; + vectorIndexingPolicy: string; + vectorEmbeddingPolicy: string; } export class AddCollectionPanel extends React.Component { @@ -136,6 +159,8 @@ export class AddCollectionPanel extends React.Component +
{this.state.errorMessage && ( )} - + {this.shouldShowVectorSearchParameters() && ( + + { + this.scrollToSection("collapsibleVectorPolicySectionContent"); + }} + > + + + Learn more + + this.setVectorIndexingPolicy(newIndexingPolicy)} + /> + + + { + this.scrollToSection("collapsibleVectorPolicySectionContent"); + }} + > + + + Learn more + + + this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy) + } + /> + + + + )} {userContext.apiType !== "Tables" && ( { TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); - this.scrollToAdvancedSection(); + this.scrollToSection("collapsibleAdvancedSectionContent"); }} > - + {isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && ( @@ -1070,6 +1160,18 @@ export class AddCollectionPanel extends React.Component
-`; \ No newline at end of file +`; diff --git a/src/Utils/CapabilityUtils.ts b/src/Utils/CapabilityUtils.ts index 216eff13d..8b6976666 100644 --- a/src/Utils/CapabilityUtils.ts +++ b/src/Utils/CapabilityUtils.ts @@ -16,3 +16,7 @@ export const isServerlessAccount = (): boolean => { isCapabilityEnabled(Constants.CapabilityNames.EnableServerless) ); }; + +export const isVectorSearchEnabled = (): boolean => { + return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch); +}; diff --git a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts index f85ddb636..049e265e9 100644 --- a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts @@ -9,7 +9,7 @@ import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -const apiVersion = "2024-02-15-preview"; +const apiVersion = "2024-05-15-preview"; /* Lists the SQL databases under an existing Azure Cosmos DB database account. */ export async function listSqlDatabases( diff --git a/src/Utils/arm/generatedClients/cosmos/types.ts b/src/Utils/arm/generatedClients/cosmos/types.ts index 4f69d223c..5272f215b 100644 --- a/src/Utils/arm/generatedClients/cosmos/types.ts +++ b/src/Utils/arm/generatedClients/cosmos/types.ts @@ -1235,6 +1235,9 @@ export interface SqlDatabaseResource { export interface SqlContainerResource { /* Name of the Cosmos DB SQL container */ id: string; + + vectorEmbeddingPolicy?: VectorEmbeddingPolicy; + /* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */ indexingPolicy?: IndexingPolicy; @@ -1267,6 +1270,17 @@ export interface SqlContainerResource { computedProperties?: ComputedProperty[]; } +export interface VectorEmbeddingPolicy { + vectorEmbeddings: VectorEmbedding[]; +} + +export interface VectorEmbedding { + path?: string; + dataType?: string; + dimensions?: number; + distanceFunction?: string; +} + /* Cosmos DB indexing policy */ export interface IndexingPolicy { /* Indicates if the indexing policy is automatic */ @@ -1285,6 +1299,13 @@ export interface IndexingPolicy { /* List of spatial specifics */ spatialIndexes?: SpatialSpec[]; + + vectorIndexes?: VectorIndex[]; +} + +export interface VectorIndex { + path?: string; + type?: string; } /* undocumented */ From 19d1e0d1df5f4206dc40301201a812c3946740d6 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Mon, 20 May 2024 19:24:04 -0500 Subject: [PATCH 41/67] allow serverless accounts to have vector search embeddings (#1844) --- .../SettingsSubComponents/ContainerVectorPolicyComponent.tsx | 2 +- src/Explorer/Panes/AddCollectionPanel.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx index 23ccdc84c..ca4d63a04 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent.tsx @@ -15,7 +15,7 @@ export const ContainerVectorPolicyComponent: React.FC Date: Wed, 29 May 2024 09:09:13 +0200 Subject: [PATCH 42/67] Migrate DocumentsTab to React and add bulk delete and column resize (#1770) * Document page now loads list of docs and displays selection * DocumentsTabV2 now properly loads documents, show partition keys and display first doc with proper selection behavior. Move it to its own folder. * Extract table in a separate component * Resizable columns on the document table * Fix selection behavior and some layout issue * Adding table scrolling * Fix NaN height issue * Fix NaN height issue * Fix column sizing + cell selection * Improvement in width size. Add Load More * Add react editor and pass column headers * Dynamic columns for pk * Fix initial columns size * Add nav buttons * Editing content updates buttons state * Discard and save buttons working * Fix save new document. Implement delete. * Remove debug display * Fix unexpand filter and reformat * Fix compil issues * Add refresh button * Update column header placeholder style * Implement delete multiple docs * Fix multi delete * Fix show/hide delete button * Fix selection behavior * Fix UX with buttons behavior and editor display * Fix UX issue with not discarding edit changes * Add some TODO's * Remove debugging info and reformat * Add mongo support * Fix build issues * Fix table header. Remove debug statement * Restore broken nosql * Fix mongo save new document/update document * Fix bugs with clicking on newly created documents * Fix comment * Fix double fetch issue when clicking on an item * Auto-select last document when saving new document * Fix resourceTokenPartitionKey code * Fix format * Fix isQueryCopilotSampleContainer flag * Fix unused code * Call tab when updating error flag * Destructure props to make useEffect dependencies work * Fix loadStartKey * minor update * Fix format * Add title to table * Fix table coming off its container with unwanted horizontal scrollbar * Increase table width. Fix eslint issue. * Move refresh documents button from table back to DocumentsTabV2 * Fix load more text centering * Don't show Load More if nothing to show * Fix columns min width * Add keyboard shortcuts * Add keyboard handlers to load more and refresh button * Add keyboard support to select table row * Disable eslint issue from fluent library * Connect cancel query button * Add Fluent V9 theme for Fabric (#1821) * Clean up dependencies and memoize internal functions and object. Move methods and object that don't depend on state outside of component. * Fix filter disappearing when clicking Apply Filter * Fix typo and format * Implement bulk delete for nosql * Replace filter ui components with fluent ui * Remove jquery calls * Migrate unit test to DocumentsTabV2 * Remove DocumentsTab and MongoDocumentsTab. Fix build issues. * Properly handle activetab * Remove comments and unused code * Port keyboard shortcuts from commitId 1f4d0f2 * Port item editor shortcuts to improved Items tab branch (#1831) * set filter focus on Ctrl+Shift+F * implement filter enter/esc keybinds * remove debugging * Collapse filter when query is executed * Fix monaco editor not happy when parent is null * Fix how bulk delete operation gets called when no partition key * Fix update id list after delete * Fix deleteDocuments * Fix build issue * Fix bug in mongo delete * Fix mongo delete flow * Proper error handling in mongo * Handle >100 bulk delete operations * Add unit tests for DocumentsTableComponent * More improvements to table unit tests * Fix import. Disable selection test for now * Add more DocumentsTab unit react tests * Remove selection test * Add more unit tests. Add lcov coverage report to display in vscode * Move unit tests to correct file * Add unit test on command bar * Fix build issues * Add more unit tests * Remove unneeded call * Add DocumentsTab for Mongo API * Fix linting errors * Update fluent ui v9 dependency. Color columns separation. Fix refresh button placement to not interfere with header cell width. * Revert @fluentui/react-components to a safe version that compiles * Add confirmation window when documents have been deleted * Fix mongo unit tests * Fix format * Update src/Common/dataAccess/deleteDocument.ts Co-authored-by: Ashley Stanton-Nurse * Update src/Common/dataAccess/deleteDocument.ts Co-authored-by: Ashley Stanton-Nurse * Update src/Common/dataAccess/deleteDocument.ts Co-authored-by: Ashley Stanton-Nurse * Fix bug with markup. Simplify code. * Protect against creating React editor without parent node * Replace rendering tests with snapshot match * Add test screenshot to troubleshoot e2e test * Revert "Add test screenshot to troubleshoot e2e test" This reverts commit 1b8138ade00571972c0e9bce77846e8fe4a3446a. * Attempt 2 at troubleshooting failing test * Revert "Attempt 2 at troubleshooting failing test" This reverts commit 3e51a593bf2001d44b6e5316d4865e6e861c21c3. * Delete button now shows if one or more rows are selected --------- Co-authored-by: Vsevolod Kukol Co-authored-by: Ashley Stanton-Nurse --- .eslintignore | 3 - jest.config.js | 2 +- less/documentDB.less | 71 +- package-lock.json | 653 ++++-- package.json | 13 +- patches/@uiw+react-split+5.9.3.patch | 11 + src/Common/DocumentUtility.ts | 6 +- src/Common/QueriesClient.ts | 7 +- src/Common/dataAccess/deleteDocument.ts | 56 + src/Explorer/Controls/Editor/EditorReact.tsx | 8 +- src/Explorer/Tabs/DocumentsTab.html | 239 --- src/Explorer/Tabs/DocumentsTab.test.ts | 152 -- src/Explorer/Tabs/DocumentsTab.ts | 1080 ---------- .../DocumentsTabV2/DocumentsTabV2.test.tsx | 476 +++++ .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 1816 +++++++++++++++++ .../DocumentsTabV2Mongo.test.tsx | 195 ++ .../DocumentsTableComponent.test.tsx | 34 + .../DocumentsTableComponent.tsx | 271 +++ .../DocumentsTabV2.test.tsx.snap | 558 +++++ .../DocumentsTableComponent.test.tsx.snap | 1140 +++++++++++ src/Explorer/Tabs/MongoDocumentsTab.ts | 320 --- src/Explorer/Tabs/useTabs.test.ts | 8 +- src/Explorer/Theme/ThemeUtil.ts | 31 + src/Explorer/Tree/Collection.ts | 19 +- src/Explorer/Tree/DocumentId.ts | 14 +- src/Explorer/Tree/ObjectId.ts | 5 +- src/Explorer/Tree/ResourceTokenCollection.ts | 10 +- src/Explorer/Tree2/ResourceTree.tsx | 30 +- src/Platform/Fabric/FabricTheme.tsx | 22 + src/Shared/Telemetry/TelemetryConstants.ts | 1 + src/setupTests.ts | 6 + 31 files changed, 5143 insertions(+), 2114 deletions(-) create mode 100644 patches/@uiw+react-split+5.9.3.patch delete mode 100644 src/Explorer/Tabs/DocumentsTab.html delete mode 100644 src/Explorer/Tabs/DocumentsTab.test.ts delete mode 100644 src/Explorer/Tabs/DocumentsTab.ts create mode 100644 src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx create mode 100644 src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx create mode 100644 src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx create mode 100644 src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.test.tsx create mode 100644 src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx create mode 100644 src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap create mode 100644 src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTableComponent.test.tsx.snap delete mode 100644 src/Explorer/Tabs/MongoDocumentsTab.ts create mode 100644 src/Explorer/Theme/ThemeUtil.ts diff --git a/.eslintignore b/.eslintignore index 78e9b517e..20cdeb4c3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -89,10 +89,7 @@ src/Explorer/Tables/TableEntityProcessor.ts src/Explorer/Tables/Utilities.ts src/Explorer/Tabs/ConflictsTab.ts src/Explorer/Tabs/DatabaseSettingsTab.ts -src/Explorer/Tabs/DocumentsTab.test.ts -src/Explorer/Tabs/DocumentsTab.ts src/Explorer/Tabs/GraphTab.ts -src/Explorer/Tabs/MongoDocumentsTab.ts src/Explorer/Tabs/NotebookV2Tab.ts src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/TabComponents.ts diff --git a/jest.config.js b/jest.config.js index b4f660063..1c1f48e6d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -31,7 +31,7 @@ module.exports = { coveragePathIgnorePatterns: ["/node_modules/"], // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: ["json", "text", "cobertura"], + coverageReporters: ["json", "text", "cobertura", "lcov"], // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { diff --git a/less/documentDB.less b/less/documentDB.less index 357d159b3..05ad22760 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2264,33 +2264,33 @@ a:link { width: 82px; } -.tabdocuments .scrollable { - height: 100%; - overflow-y: auto; - overflow-x: hidden; - padding-left: 5px; - padding-right: 5px; - width: 100%; -} +// .tabdocuments .scrollable { +// height: 100%; +// overflow-y: auto; +// overflow-x: hidden; +// padding-left: 5px; +// padding-right: 5px; +// width: 100%; +// } -.tabdocuments > .tabdocumentsGridElement { - width: 50%; -} +// .tabdocuments > .tabdocumentsGridElement { +// width: 50%; +// } -.tabdocuments > .evenlySpacedHeader { - width: 30%; -} +// .tabdocuments > .evenlySpacedHeader { +// width: 30%; +// } -.tabdocuments.scrollable:focus, -.tabdocuments.scrollable:active { - outline: 1px dotted; -} +// .tabdocuments.scrollable:focus, +// .tabdocuments.scrollable:active { +// outline: 1px dotted; +// } -.tabdocuments .scrollable table td { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} +// .tabdocuments .scrollable table td { +// white-space: nowrap; +// overflow: hidden; +// text-overflow: ellipsis; +// } .mongoDocumentEditor .monaco-editor.vs .redsquiggly { display: none !important; @@ -2316,10 +2316,9 @@ td a:hover { } .loadMore { + display: block; width: 100%; - padding-left: 30%; - padding-top: 2px; - cursor: pointer; + text-align: center; } .loadMore > a:focus { @@ -2558,10 +2557,12 @@ a:link { } .filterdivs { - padding-top: 15px; - height: 45px; - margin-bottom: 8px; + margin: 10px 0px; white-space: nowrap; + input { + line-height: 14px; // To correct vertical position of the down arrow of the input + outline: none; // Remove the dotted border on focus, because fluent has its own focus style (underlined) + } } .editFilterContainer { @@ -2578,6 +2579,18 @@ a:link { cursor: pointer; } +.documentsTab { + .documentsTable { + .documentsTableCell { + border-left: 1px solid @BaseMedium; + height: 100%; + } + .documentsTableHeader { + border-bottom: 1px solid @BaseMedium; + } + } +} + .querydropdown { border: 1px solid @BaseMedium; font-style: normal; diff --git a/package-lock.json b/package-lock.json index 26c513d6f..31e3d37db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", + "@uiw/react-split": "5.9.3", "@xmldom/xmldom": "0.7.13", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", @@ -102,6 +103,7 @@ "react-redux": "7.1.3", "react-splitter-layout": "4.0.0", "react-string-format": "1.0.1", + "react-window": "1.8.10", "react-youtube": "9.0.1", "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", @@ -128,8 +130,8 @@ "@types/datatables.net": "1.10.28", "@types/datatables.net-colreorder": "1.4.5", "@types/dom-to-image": "2.6.2", - "@types/enzyme": "3.10.7", - "@types/enzyme-adapter-react-16": "1.0.6", + "@types/enzyme": "3.10.12", + "@types/enzyme-adapter-react-16": "1.0.9", "@types/hasher": "0.0.31", "@types/jest": "26.0.20", "@types/jquery": "3.5.29", @@ -141,6 +143,7 @@ "@types/react-notification-system": "0.2.39", "@types/react-redux": "7.1.7", "@types/react-splitter-layout": "3.0.1", + "@types/react-window": "1.8.8", "@types/sanitize-html": "1.27.2", "@types/sinon": "2.3.3", "@types/styled-components": "5.1.1", @@ -156,8 +159,8 @@ "create-file-webpack": "1.0.2", "css-loader": "6.8.1", "enzyme": "3.11.0", - "enzyme-adapter-react-16": "1.15.5", - "enzyme-to-json": "3.6.1", + "enzyme-adapter-react-16": "1.15.8", + "enzyme-to-json": "3.6.2", "eslint": "8.50.0", "eslint-cli": "1.1.1", "eslint-plugin-no-null": "1.0.2", @@ -12481,9 +12484,9 @@ "integrity": "sha512-PD+wqNhrjWFjAlSVd18jvChZvOXB2SOwAILBmuYev5zswBats5qmzs/QFoooLKd2omj9BT05a8MeSeRmXLGY+Q==" }, "node_modules/@types/enzyme": { - "version": "3.10.7", - "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.7.tgz", - "integrity": "sha512-J+0wduPGAkzOvW7sr6hshGv1gBI3WXLRTczkRKzVPxLP3xAkYxZmvvagSBPw8Z452fZ8TGUxCmAXcb44yLQksw==", + "version": "3.10.12", + "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.12.tgz", + "integrity": "sha512-xryQlOEIe1TduDWAOphR0ihfebKFSWOXpIsk+70JskCfRfW+xALdnJ0r1ZOTo85F9Qsjk6vtlU7edTYHbls9tA==", "dev": true, "dependencies": { "@types/cheerio": "*", @@ -12491,9 +12494,9 @@ } }, "node_modules/@types/enzyme-adapter-react-16": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.6.tgz", - "integrity": "sha512-VonDkZ15jzqDWL8mPFIQnnLtjwebuL9YnDkqeCDYnB4IVgwUm0mwKkqhrxLL6mb05xm7qqa3IE95m8CZE9imCg==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.9.tgz", + "integrity": "sha512-z24MMxGtUL8HhXdye3tWzjp+19QTsABqLaX2oOZpxMPHRJgLfahQmOeTTrEBQd9ogW20+UmPBXD9j+XOasFHvw==", "dev": true, "dependencies": { "@types/enzyme": "*" @@ -13426,6 +13429,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -13903,6 +13915,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/react-split": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/@uiw/react-split/-/react-split-5.9.3.tgz", + "integrity": "sha512-HgwETU+kRhzZAp+YiE4Yu8bNJm3jxxnGgGPfkadUl8jA1wsMD3aMMskuhQF5akiUUUadiLUvAc8e1RH9Y/SKDw==", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@ungap/url-search-params": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@ungap/url-search-params/-/url-search-params-0.2.2.tgz", @@ -14608,12 +14632,15 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14701,15 +14728,19 @@ } }, "node_modules/array.prototype.find": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.2.tgz", - "integrity": "sha512-DRumkfW97iZGOfn+lIXbkVrXL04sfYKX+EfOodo8XboR5sxPDVvOjZTF/rysusa9lmhmSOeD6Vp6RKQP+eP4Tg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.3.tgz", + "integrity": "sha512-fO/ORdOELvjbbeIfZfzrXFMhYHGofRGqd+am9zm3tZ4GlJINj/pA2eITyfd65Vg6+ZbHd/Cys7stpoRSWtQFdA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14780,16 +14811,17 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -14914,9 +14946,12 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -15731,13 +15766,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17879,6 +17919,54 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/datatables.net": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.8.tgz", @@ -18142,16 +18230,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -18778,20 +18869,20 @@ } }, "node_modules/enzyme-adapter-react-16": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz", - "integrity": "sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==", + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.8.tgz", + "integrity": "sha512-uYGC31eGZBp5nGsr4nKhZKvxGQjyHGjS06BJsUlWgE29/hvnpgCsT1BJvnnyny7N3GIIVyxZ4O9GChr6hy2WQA==", "dev": true, "dependencies": { - "enzyme-adapter-utils": "^1.13.1", - "enzyme-shallow-equal": "^1.0.4", - "has": "^1.0.3", - "object.assign": "^4.1.0", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", + "enzyme-adapter-utils": "^1.14.2", + "enzyme-shallow-equal": "^1.0.7", + "hasown": "^2.0.0", + "object.assign": "^4.1.5", + "object.values": "^1.1.7", + "prop-types": "^15.8.1", "react-is": "^16.13.1", "react-test-renderer": "^16.0.0-0", - "semver": "^5.7.0" + "semver": "^5.7.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -18809,18 +18900,18 @@ "dev": true }, "node_modules/enzyme-adapter-utils": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.1.tgz", - "integrity": "sha512-JZgMPF1QOI7IzBj24EZoDpaeG/p8Os7WeBZWTJydpsH7JRStc7jYbHE4CmNQaLqazaGFyLM8ALWA3IIZvxW3PQ==", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.2.tgz", + "integrity": "sha512-1ZC++RlsYRaiOWE5NRaF5OgsMt7F5rn/VuaJIgc7eW/fmgg8eS1/Ut7EugSPPi7VMdWMLcymRnMF+mJUJ4B8KA==", "dev": true, "dependencies": { "airbnb-prop-types": "^2.16.0", - "function.prototype.name": "^1.1.5", - "has": "^1.0.3", - "object.assign": "^4.1.4", - "object.fromentries": "^2.0.5", + "function.prototype.name": "^1.1.6", + "hasown": "^2.0.0", + "object.assign": "^4.1.5", + "object.fromentries": "^2.0.7", "prop-types": "^15.8.1", - "semver": "^5.7.1" + "semver": "^6.3.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -18829,13 +18920,22 @@ "react": "0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0" } }, + "node_modules/enzyme-adapter-utils/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/enzyme-shallow-equal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz", - "integrity": "sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.7.tgz", + "integrity": "sha512-/um0GFqUXnpM9SvKtje+9Tjoz3f1fpBC3eXRFrNs8kpYn69JljciYP7KZTqM/YQbUY9KUjvKB4jo/q+L6WGGvg==", "dev": true, "dependencies": { - "has": "^1.0.3", + "hasown": "^2.0.0", "object-is": "^1.1.5" }, "funding": { @@ -18843,13 +18943,13 @@ } }, "node_modules/enzyme-to-json": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.1.tgz", - "integrity": "sha512-15tXuONeq5ORoZjV/bUo2gbtZrN2IH+Z6DvL35QmZyKHgbY1ahn6wcnLd9Xv9OjiwbAXiiP8MRZwbZrCv1wYNg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.2.tgz", + "integrity": "sha512-Ynm6Z6R6iwQ0g2g1YToz6DWhxVnt8Dy1ijR2zynRKxTyBGA8rCDXU3rs2Qc4OKvUvc2Qoe1bcFK6bnPs20TrTg==", "dev": true, "dependencies": { "@types/cheerio": "^0.22.22", - "lodash": "^4.17.15", + "lodash": "^4.17.21", "react-is": "^16.12.0" }, "engines": { @@ -18911,49 +19011,56 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -18967,6 +19074,25 @@ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-iterator-helpers": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", @@ -18993,14 +19119,25 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -21334,15 +21471,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -21372,12 +21513,13 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -21735,20 +21877,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -21768,11 +21910,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -21859,9 +22001,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -22826,11 +22968,11 @@ "devOptional": true }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -22916,13 +23058,15 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -23034,6 +23178,20 @@ "node": ">= 0.4" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -23209,9 +23367,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "engines": { "node": ">= 0.4" }, @@ -23348,11 +23506,14 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -23405,11 +23566,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -32745,12 +32906,12 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -33769,6 +33930,14 @@ "node": ">=0.10.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/post-robot": { "version": "10.0.42", "resolved": "https://registry.npmjs.org/post-robot/-/post-robot-10.0.42.tgz", @@ -35185,6 +35354,22 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-youtube": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-9.0.1.tgz", @@ -35553,13 +35738,14 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -36190,12 +36376,12 @@ } }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -36239,14 +36425,17 @@ } }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -36726,14 +36915,16 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -37587,13 +37778,14 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -37603,26 +37795,29 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -38708,27 +38903,28 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -38738,15 +38934,16 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -38756,13 +38953,19 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -40265,15 +40468,15 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index e1d544d1d..d27273af9 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/lodash": "4.14.171", "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", + "@uiw/react-split": "5.9.3", "@xmldom/xmldom": "0.7.13", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", @@ -98,6 +99,7 @@ "react-splitter-layout": "4.0.0", "react-string-format": "1.0.1", "react-youtube": "9.0.1", + "react-window": "1.8.10", "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", "sanitize-html": "2.3.3", @@ -123,8 +125,8 @@ "@types/datatables.net": "1.10.28", "@types/datatables.net-colreorder": "1.4.5", "@types/dom-to-image": "2.6.2", - "@types/enzyme": "3.10.7", - "@types/enzyme-adapter-react-16": "1.0.6", + "@types/enzyme": "3.10.12", + "@types/enzyme-adapter-react-16": "1.0.9", "@types/hasher": "0.0.31", "@types/jest": "26.0.20", "@types/jquery": "3.5.29", @@ -136,6 +138,7 @@ "@types/react-notification-system": "0.2.39", "@types/react-redux": "7.1.7", "@types/react-splitter-layout": "3.0.1", + "@types/react-window": "1.8.8", "@types/sanitize-html": "1.27.2", "@types/sinon": "2.3.3", "@types/styled-components": "5.1.1", @@ -151,8 +154,8 @@ "create-file-webpack": "1.0.2", "css-loader": "6.8.1", "enzyme": "3.11.0", - "enzyme-adapter-react-16": "1.15.5", - "enzyme-to-json": "3.6.1", + "enzyme-adapter-react-16": "1.15.8", + "enzyme-to-json": "3.6.2", "eslint": "8.50.0", "eslint-cli": "1.1.1", "eslint-plugin-no-null": "1.0.2", @@ -243,4 +246,4 @@ "printWidth": 120, "endOfLine": "auto" } -} +} \ No newline at end of file diff --git a/patches/@uiw+react-split+5.9.3.patch b/patches/@uiw+react-split+5.9.3.patch new file mode 100644 index 000000000..3e5307463 --- /dev/null +++ b/patches/@uiw+react-split+5.9.3.patch @@ -0,0 +1,11 @@ +diff --git a/node_modules/@uiw/react-split/cjs/index.d.ts b/node_modules/@uiw/react-split/cjs/index.d.ts +index 644bcc3..f794760 100644 +--- a/node_modules/@uiw/react-split/cjs/index.d.ts ++++ b/node_modules/@uiw/react-split/cjs/index.d.ts +@@ -56,5 +56,5 @@ export default class Split extends React.Component { + onMouseDown(paneNumber: number, env: React.MouseEvent): void; + onDragging(env: Event): void; + onDragEnd(): void; +- render(): import("react/jsx-runtime").JSX.Element; ++ render(): JSX.Element; + } diff --git a/src/Common/DocumentUtility.ts b/src/Common/DocumentUtility.ts index 99cdefc5a..322d883b0 100644 --- a/src/Common/DocumentUtility.ts +++ b/src/Common/DocumentUtility.ts @@ -1,9 +1,9 @@ import { userContext } from "../UserContext"; -export const getEntityName = (): string => { +export const getEntityName = (multiple?: boolean): string => { if (userContext.apiType === "Mongo") { - return "document"; + return multiple ? "documents" : "document"; } - return "item"; + return multiple ? "items" : "item"; }; diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index a4834b128..b56b78bb8 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -3,8 +3,7 @@ import * as _ from "underscore"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; -import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; -import DocumentId from "../Explorer/Tree/DocumentId"; +import DocumentId, { IDocumentIdContainer } from "../Explorer/Tree/DocumentId"; import { useDatabases } from "../Explorer/useDatabases"; import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; @@ -162,10 +161,10 @@ export class QueriesClient { { partitionKey: QueriesClient.PartitionKey, partitionKeyProperties: ["id"], - } as DocumentsTab, + } as IDocumentIdContainer, query, [query.queryName], - ); // TODO: Remove DocumentId's dependency on DocumentsTab + ); const options: any = { partitionKey: query.resourceId }; return deleteDocument(queriesCollection, documentId) .then( diff --git a/src/Common/dataAccess/deleteDocument.ts b/src/Common/dataAccess/deleteDocument.ts index 5caef9e0e..f20dc9cc8 100644 --- a/src/Common/dataAccess/deleteDocument.ts +++ b/src/Common/dataAccess/deleteDocument.ts @@ -1,3 +1,4 @@ +import { BulkOperationType, OperationInput } from "@azure/cosmos"; import { CollectionBase } from "../../Contracts/ViewModels"; import DocumentId from "../../Explorer/Tree/DocumentId"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; @@ -24,3 +25,58 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc clearMessage(); } }; + +/** + * Bulk delete documents + * @param collection + * @param documentId + * @returns array of ids that were successfully deleted + */ +export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise => { + const nbDocuments = documentIds.length; + const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`); + try { + const v2Container = await client().database(collection.databaseId).container(collection.id()); + + // Bulk can only delete 100 documents at a time + const BULK_DELETE_LIMIT = 100; + const promiseArray = []; + + while (documentIds.length > 0) { + const documentIdsChunk = documentIds.splice(0, BULK_DELETE_LIMIT); + const operations: OperationInput[] = documentIdsChunk.map((documentId) => ({ + id: documentId.id(), + // bulk delete: if not partition key is specified, do not pass empty array, but undefined + partitionKey: + documentId.partitionKeyValue && + Array.isArray(documentId.partitionKeyValue) && + documentId.partitionKeyValue.length === 0 + ? undefined + : documentId.partitionKeyValue, + operationType: BulkOperationType.Delete, + })); + + const promise = v2Container.items.bulk(operations).then((bulkResult) => { + return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204); + }); + promiseArray.push(promise); + } + + const allResult = await Promise.all(promiseArray); + const flatAllResult = Array.prototype.concat.apply([], allResult); + logConsoleInfo( + `Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`, + ); + // TODO: handle case result.length != nbDocuments + return flatAllResult; + } catch (error) { + handleError( + error, + "DeleteDocuments", + `Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`, + ); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index a6f487ba5..f2274d7dc 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -137,7 +137,13 @@ export class EditorReact extends React.Component - - -
-
-

Title

-
Text
-
-
- - -
-
-
-
- - - - -
- -
- SELECT * FROM c - - -
-
- Filter : - - No filter applied - - -
- - - -
-
-
- SELECT * FROM c - - - - - - - - - - - - - - Hide filter - -
-
-
- -
- - - -
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
- Refresh documents -
- -
-
-
- Load more -
- - -
-
-
-
-

Document WaterMark

-

Create new or work with existing document(s).

-
- - - -
- -
diff --git a/src/Explorer/Tabs/DocumentsTab.test.ts b/src/Explorer/Tabs/DocumentsTab.test.ts deleted file mode 100644 index c8d84e716..000000000 --- a/src/Explorer/Tabs/DocumentsTab.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as ko from "knockout"; -import { DatabaseAccount } from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { updateUserContext } from "../../UserContext"; -import Explorer from "../Explorer"; -import DocumentId from "../Tree/DocumentId"; -import DocumentsTab from "./DocumentsTab"; - -describe("Documents tab", () => { - describe("buildQuery", () => { - it("should generate the right select query for SQL API", () => { - const documentsTab = new DocumentsTab({ - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.buildQuery("")).toContain("select"); - }); - }); - - describe("showPartitionKey", () => { - const explorer = new Explorer(); - const mongoExplorer = new Explorer(); - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableGremlin" }], - }, - } as DatabaseAccount, - }); - - const collectionWithoutPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo"), - }, - container: explorer, - }); - - const collectionWithSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo"), - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: true, - }, - container: explorer, - }); - - const collectionWithNonSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo"), - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: false, - }, - container: explorer, - }); - - const mongoCollectionWithSystemPartitionKey = ({ - id: ko.observable("foo"), - database: { - id: ko.observable("foo"), - }, - partitionKey: { - paths: ["/foo"], - kind: "Hash", - version: 2, - systemKey: true, - }, - container: mongoExplorer, - }); - - it("should be false for null or undefined collection", () => { - const documentsTab = new DocumentsTab({ - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be false for null or undefined partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithoutPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be true for non-Mongo accounts with system partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(true); - }); - - it("should be false for Mongo accounts with system partitionKey", () => { - updateUserContext({ - apiType: "Mongo", - }); - const documentsTab = new DocumentsTab({ - collection: mongoCollectionWithSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(false); - }); - - it("should be true for non-system partitionKey", () => { - const documentsTab = new DocumentsTab({ - collection: collectionWithNonSystemPartitionKey, - partitionKey: null, - documentIds: ko.observableArray(), - tabKind: ViewModels.CollectionTabKind.Documents, - title: "", - tabPath: "", - }); - - expect(documentsTab.showPartitionKey).toBe(true); - }); - }); -}); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts deleted file mode 100644 index 949b13697..000000000 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ /dev/null @@ -1,1080 +0,0 @@ -import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; -import { Platform, configContext } from "ConfigContext"; -import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; -import { KeyboardAction, KeyboardActionGroup, KeyboardHandlerSetter, useKeyboardActionGroup } from "KeyboardShortcuts"; -import { QueryConstants } from "Shared/Constants"; -import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; -import * as ko from "knockout"; -import Q from "q"; -import { format } from "react-string-format"; -import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; -import NewDocumentIcon from "../../../images/NewDocument.svg"; -import UploadIcon from "../../../images/Upload_16x16.svg"; -import DiscardIcon from "../../../images/discard.svg"; -import SaveIcon from "../../../images/save-cosmos.svg"; -import * as Constants from "../../Common/Constants"; -import { - DocumentsGridMetrics, - KeyCodes, - QueryCopilotSampleContainerId, - QueryCopilotSampleDatabaseId, -} from "../../Common/Constants"; -import editable from "../../Common/EditableUtility"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as HeadersUtility from "../../Common/HeadersUtility"; -import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; -import { createDocument } from "../../Common/dataAccess/createDocument"; -import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; -import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; -import { readDocument } from "../../Common/dataAccess/readDocument"; -import { updateDocument } from "../../Common/dataAccess/updateDocument"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; -import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; -import * as QueryUtils from "../../Utils/QueryUtils"; -import { extractPartitionKeyValues } from "../../Utils/QueryUtils"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import { useDialog } from "../Controls/Dialog"; -import Explorer from "../Explorer"; -import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; -import DocumentId from "../Tree/DocumentId"; -import { useSelectedNode } from "../useSelectedNode"; -import template from "./DocumentsTab.html"; -import TabsBase from "./TabsBase"; - -export default class DocumentsTab extends TabsBase { - public readonly html = template; - public selectedDocumentId: ko.Observable; - public selectedDocumentContent: ViewModels.Editable; - public initialDocumentContent: ko.Observable; - public documentContentsGridId: string; - public documentContentsContainerId: string; - public filterContent: ko.Observable; - public appliedFilter: ko.Observable; - public lastFilterContents: ko.ObservableArray; - public isFilterExpanded: ko.Observable; - public isFilterCreated: ko.Observable; - public applyFilterButton: ViewModels.Button; - public isEditorDirty: ko.Computed; - public editorState: ko.Observable; - public newDocumentButton: ViewModels.Button; - public saveNewDocumentButton: ViewModels.Button; - public saveExistingDocumentButton: ViewModels.Button; - public discardNewDocumentChangesButton: ViewModels.Button; - public discardExisitingDocumentChangesButton: ViewModels.Button; - public deleteExisitingDocumentButton: ViewModels.Button; - public displayedError: ko.Observable; - public accessibleDocumentList: AccessibleVerticalList; - public dataContentsGridScrollHeight: ko.Observable; - public isPreferredApiMongoDB: boolean; - public shouldShowEditor: ko.Computed; - public splitter: Splitter; - public showPartitionKey: boolean; - public idHeader: string; - - // TODO need to refactor - public partitionKey: DataModels.PartitionKey; - public partitionKeyPropertyHeaders: string[]; - public partitionKeyProperties: string[]; - public documentIds: ko.ObservableArray; - - private _documentsIterator: QueryIterator; - private _resourceTokenPartitionKey: string; - private _isQueryCopilotSampleContainer: boolean; - private queryAbortController: AbortController; - private cancelQueryTimeoutID: NodeJS.Timeout; - private setKeyboardActions: KeyboardHandlerSetter; - - constructor(options: ViewModels.DocumentsTabOptions) { - super(options); - this.setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); - this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB; - - this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; - - this.documentContentsGridId = `documentContentsGrid${this.tabId}`; - this.documentContentsContainerId = `documentContentsContainer${this.tabId}`; - this.editorState = ko.observable( - ViewModels.DocumentExplorerState.noDocumentSelected, - ); - this.selectedDocumentId = ko.observable(); - this.selectedDocumentContent = editable.observable(""); - this.initialDocumentContent = ko.observable(""); - this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey); - this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; - this.documentIds = options.documentIds; - - this.partitionKeyPropertyHeaders = this.collection?.partitionKeyPropertyHeaders || this.partitionKey?.paths; - this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => - partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), - ); - - this.isFilterExpanded = ko.observable(false); - this.isFilterCreated = ko.observable(true); - this.filterContent = ko.observable(""); - this.appliedFilter = ko.observable(""); - this.displayedError = ko.observable(""); - this.lastFilterContents = ko.observableArray([ - 'WHERE c.id = "foo"', - "ORDER BY c._ts DESC", - 'WHERE c.id = "foo" ORDER BY c._ts DESC', - ]); - - this.dataContentsGridScrollHeight = ko.observable(null); - - // initialize splitter only after template has been loaded so dom elements are accessible - super.onTemplateReady((isTemplateReady: boolean) => { - if (isTemplateReady) { - const tabContainer: HTMLElement = document.getElementById("content"); - const splitterBounds: SplitterBounds = { - min: Constants.DocumentsGridMetrics.DocumentEditorMinWidthRatio * tabContainer.clientWidth, - max: Constants.DocumentsGridMetrics.DocumentEditorMaxWidthRatio * tabContainer.clientWidth, - }; - this.splitter = new Splitter({ - splitterId: "h_splitter2", - leftId: this.documentContentsContainerId, - bounds: splitterBounds, - direction: SplitterDirection.Vertical, - }); - } - }); - - this.accessibleDocumentList = new AccessibleVerticalList(this.documentIds()); - this.accessibleDocumentList.setOnSelect( - (selectedDocument: DocumentId) => selectedDocument && selectedDocument.click(), - ); - this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) => - this.accessibleDocumentList.updateCurrentItem(newSelectedDocumentId), - ); - this.documentIds.subscribe((newDocuments: DocumentId[]) => { - this.accessibleDocumentList.updateItemList(newDocuments); - if (newDocuments.length > 0) { - this.dataContentsGridScrollHeight( - newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px", - ); - } else { - this.dataContentsGridScrollHeight( - DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px", - ); - } - }); - - this.isEditorDirty = ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - return false; - - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - return true; - - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return ( - this.selectedDocumentContent.getEditableOriginalValue() !== - this.selectedDocumentContent.getEditableCurrentValue() - ); - - default: - return false; - } - }); - - this.newDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - return true; - }), - }; - - this.saveNewDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }), - }; - - this.discardNewDocumentChangesButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - return true; - } - - return false; - }), - }; - - this.saveExistingDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - }; - - this.discardExisitingDocumentChangesButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - }; - - this.deleteExisitingDocumentButton = { - enabled: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - - visible: ko.computed(() => { - switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - } - - return false; - }), - }; - - this.applyFilterButton = { - enabled: ko.computed(() => { - return true; - }), - - visible: ko.computed(() => { - return true; - }), - }; - this.buildCommandBarOptions(); - this.shouldShowEditor = ko.computed(() => { - const documentHasContent: boolean = - this.selectedDocumentContent() != null && this.selectedDocumentContent().length > 0; - const isNewDocument: boolean = - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid; - - return documentHasContent || isNewDocument; - }); - this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent)); - - this.showPartitionKey = this._shouldShowPartitionKey(); - this._isQueryCopilotSampleContainer = - this.collection?.isSampleCollection && - this.collection?.databaseId === QueryCopilotSampleDatabaseId && - this.collection?.id() === QueryCopilotSampleContainerId; - } - - private _shouldShowPartitionKey(): boolean { - if (!this.collection) { - return false; - } - - if (!this.collection.partitionKey) { - return false; - } - - if (this.collection.partitionKey.systemKey && this.isPreferredApiMongoDB) { - return false; - } - - return true; - } - - /** - * Query first page of documents - * Select and query first document and display content - */ - private async autoPopulateContent(applyFilterButtonPressed?: boolean) { - // reset iterator - this._documentsIterator = this.createIterator(); - // load documents - await this.loadNextPage(applyFilterButtonPressed); - - // Select first document and load content - if (this.documentIds().length > 0) { - this.documentIds()[0].click(); - } - } - - public onShowFilterClick(): Q.Promise { - this.isFilterCreated(true); - this.isFilterExpanded(true); - - $(".filterDocExpanded").addClass("active"); - $("#content").addClass("active"); - $(".querydropdown").focus(); - - return Q(); - } - - public onHideFilterClick(): Q.Promise { - this.isFilterExpanded(false); - - $(".filterDocExpanded").removeClass("active"); - $("#content").removeClass("active"); - $(".queryButton").focus(); - return Q(); - } - - public onCloseButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.onHideFilterClick(); - event.stopPropagation(); - return false; - } - return true; - }; - - public async refreshDocumentsGrid(applyFilterButtonPressed?: boolean): Promise { - // clear documents grid - this.documentIds([]); - try { - // reset iterator - this._documentsIterator = this.createIterator(); - // load documents - await this.autoPopulateContent(applyFilterButtonPressed); - // collapse filter - this.appliedFilter(this.filterContent()); - this.isFilterExpanded(false); - document.getElementById("errorStatusIcon")?.focus(); - } catch (error) { - useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); - } - } - - public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - this.refreshDocumentsGrid(); - event.stopPropagation(); - return false; - } - return true; - }; - - public onAbortQueryClick(): void { - this.queryAbortController.abort(); - } - - /** - * TODO Doesn't seem to be used: remove? - * @param clickedDocumentId - * @returns - */ - public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise { - if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) { - return Q(); - } - - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - - return Q(); - } - - public onNewDocumentClick = (): void => { - if (this.isEditorDirty()) { - useDialog - .getState() - .showOkCancelModalDialog( - "Unsaved changes", - "Changes will be lost. Do you want to continue?", - "OK", - () => this.initializeNewDocument(), - "Cancel", - undefined, - ); - } else { - this.initializeNewDocument(); - } - }; - - private initializeNewDocument = (): void => { - this.selectedDocumentId(null); - const newDocument: any = { - id: "replace_with_new_document_id", - }; - this.partitionKeyProperties.forEach((partitionKeyProperty) => { - let target = newDocument; - const keySegments = partitionKeyProperty.split("."); - const finalSegment = keySegments.pop(); - - // Initialize nested objects as needed - keySegments.forEach((segment) => { - target = target[segment] = target[segment] || {}; - }); - - target[finalSegment] = "replace_with_new_partition_key_value"; - }); - const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4); - this.initialDocumentContent(defaultDocument); - this.selectedDocumentContent.setBaseline(defaultDocument); - this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - }; - - public onSaveNewDocumentClick = (): Promise => { - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - const document = JSON.parse(this.selectedDocumentContent()); - this.isExecuting(true); - return createDocument(this.collection, document) - .then( - (savedDocument: any) => { - const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - this.initialDocumentContent(value); - const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( - savedDocument, - this.partitionKey as PartitionKeyDefinition, - ); - let id = new DocumentId(this, savedDocument, partitionKeyValueArray); - let ids = this.documentIds(); - ids.push(id); - - this.selectedDocumentId(id); - this.documentIds(ids); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - useDialog.getState().showOkModalDialog("Create document failed", errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - }; - - public onRevertNewDocumentClick = (): Q.Promise => { - this.initialDocumentContent(""); - this.selectedDocumentContent(""); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - - return Q(); - }; - - public onSaveExistingDocumentClick = (): Promise => { - const selectedDocumentId = this.selectedDocumentId(); - const documentContent = JSON.parse(this.selectedDocumentContent()); - - const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( - documentContent, - this.partitionKey as PartitionKeyDefinition, - ); - selectedDocumentId.partitionKeyValue = partitionKeyValueArray; - - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - this.isExecuting(true); - return updateDocument(this.collection, selectedDocumentId, documentContent) - .then( - (updatedDocument: any) => { - const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - this.initialDocumentContent(value); - this.documentIds().forEach((documentId: DocumentId) => { - if (documentId.rid === updatedDocument._rid) { - documentId.id(updatedDocument.id); - } - }); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - useDialog.getState().showOkModalDialog("Update document failed", errorMessage); - TelemetryProcessor.traceFailure( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - }; - - public onRevertExisitingDocumentClick = (): Q.Promise => { - this.selectedDocumentContent.setBaseline(this.initialDocumentContent()); - this.initialDocumentContent.valueHasMutated(); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - - return Q(); - }; - - public onDeleteExisitingDocumentClick = async (): Promise => { - const selectedDocumentId = this.selectedDocumentId(); - const msg = !this.isPreferredApiMongoDB - ? "Are you sure you want to delete the selected item ?" - : "Are you sure you want to delete the selected document ?"; - - useDialog - .getState() - .showOkCancelModalDialog( - "Confirm delete", - msg, - "Delete", - async () => await this._deleteDocument(selectedDocumentId), - "Cancel", - undefined, - ); - }; - - public onValidDocumentEdit(): Q.Promise { - if ( - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid - ) { - this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - return Q(); - } - - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); - return Q(); - } - - public onInvalidDocumentEdit(): Q.Promise { - if ( - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid - ) { - this.editorState(ViewModels.DocumentExplorerState.newDocumentInvalid); - return Q(); - } - - if ( - this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || - this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid - ) { - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); - return Q(); - } - - return Q(); - } - - public onTabClick(): void { - super.onTabClick(); - this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); - } - - public onFilterKeyDown(model: unknown, e: KeyboardEvent): boolean { - if (e.key === "Enter") { - this.refreshDocumentsGrid(true); - - // Suppress the default behavior of the key - return false; - } else if (e.key === "Escape") { - this.onHideFilterClick(); - - // Suppress the default behavior of the key - return false; - } else { - // Allow the default behavior of the key - return true; - } - } - - public async onActivate(): Promise { - super.onActivate(); - - this.setKeyboardActions({ - [KeyboardAction.SEARCH]: () => { - this.onShowFilterClick(); - return true; - }, - [KeyboardAction.CLEAR_SEARCH]: () => { - this.filterContent(""); - this.refreshDocumentsGrid(true); - return true; - }, - }); - - if (!this._documentsIterator) { - try { - await this.autoPopulateContent(); - } catch (error) { - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - } - } - } - - protected __deleteDocument(documentId: DocumentId): Promise { - return deleteDocument(this.collection, documentId); - } - - private _deleteDocument(selectedDocumentId: DocumentId): Promise { - this.isExecutionError(false); - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - this.isExecuting(true); - return this.__deleteDocument(selectedDocumentId) - .then( - () => { - this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); - this.selectedDocumentContent(""); - this.selectedDocumentId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - TelemetryProcessor.traceSuccess( - Action.DeleteDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - console.error(error); - TelemetryProcessor.traceFailure( - Action.DeleteDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - } - - public createIterator(): QueryIterator { - this.queryAbortController = new AbortController(); - const filter: string = this.filterContent().trim(); - const query: string = this.buildQuery(filter); - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - - if (this._resourceTokenPartitionKey) { - options.partitionKey = this._resourceTokenPartitionKey; - } - options.abortSignal = this.queryAbortController.signal; - return this._isQueryCopilotSampleContainer - ? querySampleDocuments(query, options) - : queryDocuments(this.collection.databaseId, this.collection.id(), query, options); - } - - public async selectDocument(documentId: DocumentId): Promise { - this.selectedDocumentId(documentId); - const content = await (this._isQueryCopilotSampleContainer - ? readSampleDocument(documentId) - : readDocument(this.collection, documentId)); - this.initDocumentEditor(documentId, content); - } - - public loadNextPage(applyFilterButtonClicked?: boolean): Q.Promise { - this.isExecuting(true); - this.isExecutionError(false); - let automaticallyCancelQueryAfterTimeout: boolean; - if (applyFilterButtonClicked && this.queryTimeoutEnabled()) { - const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout); - automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean( - StorageKey.AutomaticallyCancelQueryAfterTimeout, - ); - const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => { - if (this.isExecuting()) { - if (automaticallyCancelQueryAfterTimeout) { - this.queryAbortController.abort(); - } else { - useDialog - .getState() - .showOkCancelModalDialog( - QueryConstants.CancelQueryTitle, - format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached), - "Yes", - () => this.queryAbortController.abort(), - "No", - undefined, - ); - } - } - }, queryTimeout); - this.cancelQueryTimeoutID = cancelQueryTimeoutID; - } - return this._loadNextPageInternal() - .then( - (documentsIdsResponse = []) => { - const currentDocuments = this.documentIds(); - const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); - const nextDocumentIds = documentsIdsResponse - // filter documents already loaded in observable - .filter((d: any) => { - return currentDocumentsRids.indexOf(d._rid) < 0; - }) - // map raw response to view model - .map((rawDocument: any) => { - const partitionKeyValue = rawDocument._partitionKeyValue; - return new DocumentId(this, rawDocument, partitionKeyValue); - }); - - const merged = currentDocuments.concat(nextDocumentIds); - this.documentIds(merged); - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceSuccess( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - logConsoleError(errorMessage); - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - }, - ) - .finally(() => { - this.isExecuting(false); - if (applyFilterButtonClicked && this.queryTimeoutEnabled()) { - clearTimeout(this.cancelQueryTimeoutID); - if (!automaticallyCancelQueryAfterTimeout) { - useDialog.getState().closeDialog(); - } - } - }); - } - - public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => { - if (event.key === " " || event.key === "Enter") { - const focusElement = document.getElementById(this.documentContentsGridId); - this.loadNextPage(); - focusElement && focusElement.focus(); - event.stopPropagation(); - event.preventDefault(); - } - }; - - protected _loadNextPageInternal(): Q.Promise { - return Q(this._documentsIterator.fetchNext().then((response) => response.resources)); - } - - protected _onEditorContentChange(newContent: string) { - try { - let parsed: any = JSON.parse(newContent); - this.onValidDocumentEdit(); - } catch (e) { - this.onInvalidDocumentEdit(); - } - } - - public initDocumentEditor(documentId: DocumentId, documentContent: any): Q.Promise { - if (documentId) { - const content: string = this.renderObjectForEditor(documentContent, null, 4); - this.selectedDocumentContent.setBaseline(content); - this.initialDocumentContent(content); - const newState = documentId - ? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits - : ViewModels.DocumentExplorerState.newDocumentValid; - this.editorState(newState); - } - - return Q(); - } - - public buildQuery(filter: string): string { - return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperties, this.partitionKey); - } - - protected getTabsButtons(): CommandButtonComponentProps[] { - if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { - // All the following buttons require write access - return []; - } - - const buttons: CommandButtonComponentProps[] = []; - const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; - if (this.newDocumentButton.visible()) { - buttons.push({ - iconSrc: NewDocumentIcon, - iconAlt: label, - keyboardAction: KeyboardAction.NEW_ITEM, - onCommandClick: this.onNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: !this.newDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(), - id: "mongoNewDocumentBtn", - }); - } - - if (this.saveNewDocumentButton.visible()) { - const label = "Save"; - buttons.push({ - iconSrc: SaveIcon, - iconAlt: label, - keyboardAction: KeyboardAction.SAVE_ITEM, - onCommandClick: this.onSaveNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.saveNewDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (this.discardNewDocumentChangesButton.visible()) { - const label = "Discard"; - buttons.push({ - iconSrc: DiscardIcon, - iconAlt: label, - keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, - onCommandClick: this.onRevertNewDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.discardNewDocumentChangesButton.enabled() || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (this.saveExistingDocumentButton.visible()) { - const label = "Update"; - buttons.push({ - iconSrc: SaveIcon, - iconAlt: label, - keyboardAction: KeyboardAction.SAVE_ITEM, - onCommandClick: this.onSaveExistingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.saveExistingDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (this.discardExisitingDocumentChangesButton.visible()) { - const label = "Discard"; - buttons.push({ - iconSrc: DiscardIcon, - iconAlt: label, - keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, - onCommandClick: this.onRevertExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.discardExisitingDocumentChangesButton.enabled() || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (this.deleteExisitingDocumentButton.visible()) { - const label = "Delete"; - buttons.push({ - iconSrc: DeleteDocumentIcon, - iconAlt: label, - keyboardAction: KeyboardAction.DELETE_ITEM, - onCommandClick: this.onDeleteExisitingDocumentClick, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: false, - disabled: - !this.deleteExisitingDocumentButton.enabled() || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }); - } - - if (!this.isPreferredApiMongoDB) { - buttons.push(DocumentsTab._createUploadButton(this.collection.container)); - } - - return buttons; - } - - protected buildCommandBarOptions(): void { - ko.computed(() => - ko.toJSON([ - this.newDocumentButton.visible, - this.newDocumentButton.enabled, - this.saveNewDocumentButton.visible, - this.saveNewDocumentButton.enabled, - this.discardNewDocumentChangesButton.visible, - this.discardNewDocumentChangesButton.enabled, - this.saveExistingDocumentButton.visible, - this.saveExistingDocumentButton.enabled, - this.discardExisitingDocumentChangesButton.visible, - this.discardExisitingDocumentChangesButton.enabled, - this.deleteExisitingDocumentButton.visible, - this.deleteExisitingDocumentButton.enabled, - ]), - ).subscribe(() => this.updateNavbarWithTabsButtons()); - this.updateNavbarWithTabsButtons(); - } - - public static _createUploadButton(container: Explorer): CommandButtonComponentProps { - const label = "Upload Item"; - return { - id: "uploadItemBtn", - iconSrc: UploadIcon, - iconAlt: label, - onCommandClick: () => { - const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); - selectedCollection && container.openUploadItemsPanePane(); - }, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: true, - disabled: - useSelectedNode.getState().isDatabaseNodeOrNoneSelected() || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }; - } - - private queryTimeoutEnabled(): boolean { - return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled); - } -} diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx new file mode 100644 index 000000000..d224c2f9a --- /dev/null +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx @@ -0,0 +1,476 @@ +import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos"; +import { deleteDocuments } from "Common/dataAccess/deleteDocument"; +import { Platform, updateConfigContext } from "ConfigContext"; +import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact"; +import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { + ButtonsDependencies, + DELETE_BUTTON_ID, + DISCARD_BUTTON_ID, + DocumentsTabComponent, + IDocumentsTabComponentProps, + NEW_DOCUMENT_BUTTON_ID, + SAVE_BUTTON_ID, + UPDATE_BUTTON_ID, + UPLOAD_BUTTON_ID, + buildQuery, + getDiscardExistingDocumentChangesButtonState, + getDiscardNewDocumentChangesButtonState, + getSaveExistingDocumentButtonState, + getSaveNewDocumentButtonState, + getTabsButtons, + showPartitionKey, +} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; +import { ReactWrapper, ShallowWrapper, mount, shallow } from "enzyme"; +import * as ko from "knockout"; +import React from "react"; +import { act } from "react-dom/test-utils"; +import { DatabaseAccount, DocumentId } from "../../../Contracts/DataModels"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { updateUserContext } from "../../../UserContext"; +import Explorer from "../../Explorer"; + +jest.mock("Common/dataAccess/queryDocuments", () => ({ + queryDocuments: jest.fn(() => ({ + // Omit headers, because we can't mock a private field and we don't need to test it + fetchNext: (): Promise, "headers">> => + Promise.resolve({ + resources: [{ id: "id", _rid: "rid", _self: "self", _etag: "etag", _ts: 123 }], + hasMoreResults: false, + diagnostics: undefined, + + continuation: undefined, + continuationToken: undefined, + queryMetrics: "queryMetrics", + requestCharge: 1, + activityId: "activityId", + indexMetrics: "indexMetrics", + }), + })), +})); + +const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__"; +jest.mock("Common/dataAccess/readDocument", () => ({ + readDocument: jest.fn(() => + Promise.resolve({ + container: undefined, + id: "id", + property: PROPERTY_VALUE, + }), + ), +})); + +jest.mock("Explorer/Controls/Editor/EditorReact", () => ({ + EditorReact: (props: EditorReactProps) => <>{props.content}, +})); + +jest.mock("Explorer/Controls/Dialog", () => ({ + useDialog: { + getState: jest.fn(() => ({ + showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(), + showOkModalDialog: () => {}, + })), + }, +})); + +jest.mock("Common/dataAccess/deleteDocument", () => ({ + deleteDocuments: jest.fn((collection: ViewModels.CollectionBase, documentIds: DocumentId[]) => + Promise.resolve(documentIds), + ), +})); + +async function waitForComponentToPaint

(wrapper: ReactWrapper

| ShallowWrapper

, amount = 0) { + let newWrapper; + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, amount)); + newWrapper = wrapper.update(); + }); + return newWrapper; +} + +describe("Documents tab (noSql API)", () => { + describe("buildQuery", () => { + it("should generate the right select query for SQL API", () => { + expect(buildQuery(false, "")).toContain("select"); + }); + }); + + describe("showPartitionKey", () => { + const explorer = new Explorer(); + const mongoExplorer = new Explorer(); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableGremlin" }], + }, + } as DatabaseAccount, + }); + + const collectionWithoutPartitionKey: ViewModels.Collection = { + id: ko.observable("foo"), + databaseId: "foo", + container: explorer, + } as ViewModels.Collection; + + const collectionWithSystemPartitionKey: ViewModels.Collection = { + id: ko.observable("foo"), + databaseId: "foo", + partitionKey: { + paths: ["/foo"], + kind: "Hash", + version: 2, + systemKey: true, + }, + container: explorer, + } as ViewModels.Collection; + + const collectionWithNonSystemPartitionKey: ViewModels.Collection = { + id: ko.observable("foo"), + databaseId: "foo", + partitionKey: { + paths: ["/foo"], + kind: "Hash", + version: 2, + systemKey: false, + }, + container: explorer, + } as ViewModels.Collection; + + const mongoCollectionWithSystemPartitionKey: ViewModels.Collection = { + id: ko.observable("foo"), + databaseId: "foo", + partitionKey: { + paths: ["/foo"], + kind: "Hash", + version: 2, + systemKey: true, + }, + container: mongoExplorer, + } as ViewModels.Collection; + + it("should be false for null or undefined collection", () => { + expect(showPartitionKey(undefined, false)).toBe(false); + expect(showPartitionKey(null, false)).toBe(false); + expect(showPartitionKey(undefined, true)).toBe(false); + expect(showPartitionKey(null, true)).toBe(false); + }); + + it("should be false for null or undefined partitionKey", () => { + expect(showPartitionKey(collectionWithoutPartitionKey, false)).toBe(false); + }); + + it("should be true for non-Mongo accounts with system partitionKey", () => { + expect(showPartitionKey(collectionWithSystemPartitionKey, false)).toBe(true); + }); + + it("should be false for Mongo accounts with system partitionKey", () => { + expect(showPartitionKey(mongoCollectionWithSystemPartitionKey, true)).toBe(false); + }); + + it("should be true for non-system partitionKey", () => { + expect(showPartitionKey(collectionWithNonSystemPartitionKey, false)).toBe(true); + }); + }); + + describe("when getting command bar button state", () => { + describe("should set Save New Document state", () => { + const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>(); + testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: true, visible: true }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: true }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, + enabled: false, + visible: false, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + enabled: false, + visible: false, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + enabled: false, + visible: false, + }); + + testCases.forEach((testCase) => { + const state = getSaveNewDocumentButtonState(testCase.state); + it(`enable for ${testCase.state}`, () => { + expect(state.enabled).toBe(testCase.enabled); + }); + it(`visible for ${testCase.state}`, () => { + expect(state.visible).toBe(testCase.visible); + }); + }); + }); + + describe("should set Discard New Document state", () => { + const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>(); + testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: true, visible: true }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: true, visible: true }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, + enabled: false, + visible: false, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + enabled: false, + visible: false, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + enabled: false, + visible: false, + }); + + testCases.forEach((testCase) => { + const state = getDiscardNewDocumentChangesButtonState(testCase.state); + it(`enable for ${testCase.state}`, () => { + expect(state.enabled).toBe(testCase.enabled); + }); + it(`visible for ${testCase.state}`, () => { + expect(state.visible).toBe(testCase.visible); + }); + }); + }); + + describe("should set Save Existing Document state", () => { + const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>(); + testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, + enabled: false, + visible: true, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + enabled: true, + visible: true, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + enabled: false, + visible: true, + }); + + testCases.forEach((testCase) => { + const state = getSaveExistingDocumentButtonState(testCase.state); + it(`enable for ${testCase.state}`, () => { + expect(state.enabled).toBe(testCase.enabled); + }); + it(`visible for ${testCase.state}`, () => { + expect(state.visible).toBe(testCase.visible); + }); + }); + }); + + describe("should set Discard Existing Document state", () => { + const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>(); + testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, + enabled: false, + visible: true, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + enabled: true, + visible: true, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + enabled: true, + visible: true, + }); + + testCases.forEach((testCase) => { + const state = getDiscardExistingDocumentChangesButtonState(testCase.state); + it(`enable for ${testCase.state}`, () => { + expect(state.enabled).toBe(testCase.enabled); + }); + it(`visible for ${testCase.state}`, () => { + expect(state.visible).toBe(testCase.visible); + }); + }); + }); + + describe("should set Delete Existing Document state", () => { + const testCases = new Set<{ state: ViewModels.DocumentExplorerState; enabled: boolean; visible: boolean }>(); + testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false }); + testCases.add({ state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, enabled: true, visible: true }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + enabled: true, + visible: true, + }); + testCases.add({ + state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + enabled: true, + visible: true, + }); + }); + }); + + it("Do not get tabs button for Fabric readonly", () => { + updateConfigContext({ platform: Platform.Fabric }); + updateUserContext({ + fabricContext: { + connectionId: "test", + databaseConnectionInfo: undefined, + isReadOnly: true, + isVisible: true, + }, + }); + + const buttons = getTabsButtons({} as ButtonsDependencies); + expect(buttons.length).toBe(0); + }); + + describe("when rendered", () => { + const createMockProps = (): IDocumentsTabComponentProps => ({ + isPreferredApiMongoDB: false, + documentIds: [], + collection: undefined, + partitionKey: undefined, + onLoadStartKey: 0, + tabTitle: "", + onExecutionErrorChange: (isExecutionError: boolean): void => { + isExecutionError; + }, + onIsExecutingChange: (isExecuting: boolean): void => { + isExecuting; + }, + isTabActive: true, + }); + + let wrapper: ShallowWrapper; + + beforeEach(async () => { + const props: IDocumentsTabComponentProps = createMockProps(); + wrapper = shallow(); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + it("should render the page", () => { + expect(wrapper).toMatchSnapshot(); + }); + + it("clicking on Edit filter should render the Apply Filter button", () => { + wrapper + .findWhere((node) => node.text() === "Edit Filter") + .at(0) + .simulate("click"); + expect(wrapper.findWhere((node) => node.text() === "Apply Filter").exists()).toBeTruthy(); + }); + + it("clicking on Edit filter should render input for filter", () => { + wrapper + .findWhere((node) => node.text() === "Edit Filter") + .at(0) + .simulate("click"); + expect(wrapper.find("#filterInput").exists()).toBeTruthy(); + }); + }); + + describe("Command bar buttons", () => { + const createMockProps = (): IDocumentsTabComponentProps => ({ + isPreferredApiMongoDB: false, + documentIds: [], + collection: { + id: ko.observable("foo"), + container: new Explorer(), + partitionKey: { + kind: "MultiHash", + paths: ["/pkey1", "/pkey2", "/pkey3"], + version: 2, + }, + partitionKeyProperties: ["pkey1", "pkey2", "pkey3"], + partitionKeyPropertyHeaders: ["/pkey1", "/pkey2", "/pkey3"], + } as ViewModels.CollectionBase, + partitionKey: undefined, + onLoadStartKey: 0, + tabTitle: "", + onExecutionErrorChange: (isExecutionError: boolean): void => { + isExecutionError; + }, + onIsExecutingChange: (isExecuting: boolean): void => { + isExecuting; + }, + isTabActive: true, + }); + + let wrapper: ReactWrapper; + + beforeEach(async () => { + updateConfigContext({ platform: Platform.Hosted }); + + const props: IDocumentsTabComponentProps = createMockProps(); + + wrapper = mount(); + wrapper = await waitForComponentToPaint(wrapper); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + it("renders by default the first document", async () => { + expect(wrapper.findWhere((node) => node.text().includes(PROPERTY_VALUE)).exists()).toBeTruthy(); + }); + + it("default buttons", async () => { + expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPDATE_BUTTON_ID)).toBeDefined(); + expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined(); + expect(useCommandBar.getState().contextButtons.find((button) => button.id === DELETE_BUTTON_ID)).toBeDefined(); + expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPLOAD_BUTTON_ID)).toBeDefined(); + }); + + it("clicking on New Document should show editor with new document", () => { + act(() => { + useCommandBar + .getState() + .contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID) + .onCommandClick(undefined); + }); + expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy(); + }); + + it("clicking on New Document should show Save and Discard buttons", () => { + act(() => { + useCommandBar + .getState() + .contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID) + .onCommandClick(undefined); + }); + + expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined(); + expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined(); + }); + + it("clicking Delete Document asks for confirmation", () => { + const mockDeleteDocuments = deleteDocuments as jest.Mock; + mockDeleteDocuments.mockClear(); + + act(() => { + useCommandBar + .getState() + .contextButtons.find((button) => button.id === DELETE_BUTTON_ID) + .onCommandClick(undefined); + }); + + expect(mockDeleteDocuments).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx new file mode 100644 index 000000000..1fb6ed9db --- /dev/null +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -0,0 +1,1816 @@ +import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import { Button, FluentProvider, Input, TableRowId } from "@fluentui/react-components"; +import { ArrowClockwise16Filled, Dismiss16Filled } from "@fluentui/react-icons"; +import Split from "@uiw/react-split"; +import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; +import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; +import MongoUtility from "Common/MongoUtility"; +import { StyleConstants } from "Common/StyleConstants"; +import { createDocument } from "Common/dataAccess/createDocument"; +import { deleteDocuments as deleteNoSqlDocuments } from "Common/dataAccess/deleteDocument"; +import { queryDocuments } from "Common/dataAccess/queryDocuments"; +import { readDocument } from "Common/dataAccess/readDocument"; +import { updateDocument } from "Common/dataAccess/updateDocument"; +import { Platform, configContext } from "ConfigContext"; +import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "Explorer/Controls/Dialog"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; +import Explorer from "Explorer/Explorer"; +import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; +import { getPlatformTheme } from "Explorer/Theme/ThemeUtil"; +import { useSelectedNode } from "Explorer/useSelectedNode"; +import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; +import { QueryConstants } from "Shared/Constants"; +import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; +import { userContext } from "UserContext"; +import { logConsoleError } from "Utils/NotificationConsoleUtils"; +import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { format } from "react-string-format"; +import { CSSProperties } from "styled-components"; +import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg"; +import NewDocumentIcon from "../../../../images/NewDocument.svg"; +import UploadIcon from "../../../../images/Upload_16x16.svg"; +import DiscardIcon from "../../../../images/discard.svg"; +import SaveIcon from "../../../../images/save-cosmos.svg"; +import * as Constants from "../../../Common/Constants"; +import * as HeadersUtility from "../../../Common/HeadersUtility"; +import * as Logger from "../../../Common/Logger"; +import * as MongoProxyClient from "../../../Common/MongoProxyClient"; +import * as DataModels from "../../../Contracts/DataModels"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import * as QueryUtils from "../../../Utils/QueryUtils"; +import { extractPartitionKeyValues } from "../../../Utils/QueryUtils"; +import DocumentId from "../../Tree/DocumentId"; +import ObjectId from "../../Tree/ObjectId"; +import TabsBase from "../TabsBase"; +import { DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent"; + +export class DocumentsTabV2 extends TabsBase { + public partitionKey: DataModels.PartitionKey; + private documentIds: DocumentId[]; + private title: string; + private resourceTokenPartitionKey: string; + + constructor(options: ViewModels.DocumentsTabOptions) { + super(options); + + this.documentIds = options.documentIds(); + this.title = options.title; + this.partitionKey = options.partitionKey; + this.resourceTokenPartitionKey = options.resourceTokenPartitionKey; + } + + public render(): JSX.Element { + return ( + this.isExecutionError(isExecutionError)} + onIsExecutingChange={(isExecuting: boolean) => this.isExecuting(isExecuting)} + isTabActive={this.isActive()} + /> + ); + } + + public onActivate(): void { + super.onActivate(); + this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); + } +} + +const filterButtonStyle: CSSProperties = { + marginLeft: 8, +}; + +// From TabsBase.renderObjectForEditor() +let renderObjectForEditor = ( + value: unknown, + replacer: (this: unknown, key: string, value: unknown) => unknown, + space: string | number, +): string => JSON.stringify(value, replacer, space); + +// Export to expose to unit tests +export const getSaveNewDocumentButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ + enabled: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.newDocumentValid: + return true; + default: + return false; + } + })(), + + visible: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + return true; + default: + return false; + } + })(), +}); + +// Export to expose to unit tests +export const getDiscardNewDocumentChangesButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ + enabled: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + return true; + default: + return false; + } + })(), + + visible: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + return true; + default: + return false; + } + })(), +}); + +// Export to expose to unit tests +export const getSaveExistingDocumentButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ + enabled: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + default: + return false; + } + })(), + + visible: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + default: + return false; + } + })(), +}); + +// Export to expose to unit tests +export const getDiscardExistingDocumentChangesButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ + enabled: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + default: + return false; + } + })(), + + visible: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + default: + return false; + } + })(), +}); + +type UiKeyboardEvent = (e: KeyboardEvent | React.SyntheticEvent) => void; + +// Export to expose to unit tests +export type ButtonsDependencies = { + _collection: ViewModels.CollectionBase; + selectedRows: Set; + editorState: ViewModels.DocumentExplorerState; + isPreferredApiMongoDB: boolean; + onNewDocumentClick: UiKeyboardEvent; + onSaveNewDocumentClick: UiKeyboardEvent; + onRevertNewDocumentClick: UiKeyboardEvent; + onSaveExistingDocumentClick: UiKeyboardEvent; + onRevertExistingDocumentClick: UiKeyboardEvent; + onDeleteExistingDocumentsClick: UiKeyboardEvent; +}; + +const createUploadButton = (container: Explorer): CommandButtonComponentProps => { + const label = "Upload Item"; + return { + id: UPLOAD_BUTTON_ID, + iconSrc: UploadIcon, + iconAlt: label, + onCommandClick: () => { + const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); + selectedCollection && container.openUploadItemsPanePane(); + }, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: true, + disabled: + useSelectedNode.getState().isDatabaseNodeOrNoneSelected() || + useSelectedNode.getState().isQueryCopilotCollectionSelected(), + }; +}; + +// Export to expose to unit tests +export const NEW_DOCUMENT_BUTTON_ID = "mongoNewDocumentBtn"; +export const SAVE_BUTTON_ID = "saveBtn"; +export const UPDATE_BUTTON_ID = "updateBtn"; +export const DISCARD_BUTTON_ID = "discardBtn"; +export const DELETE_BUTTON_ID = "deleteBtn"; +export const UPLOAD_BUTTON_ID = "uploadItemBtn"; + +// Export to expose in unit tests +export const getTabsButtons = ({ + _collection, + selectedRows, + editorState, + isPreferredApiMongoDB, + onNewDocumentClick, + onSaveNewDocumentClick, + onRevertNewDocumentClick, + onSaveExistingDocumentClick, + onRevertExistingDocumentClick, + onDeleteExistingDocumentsClick, +}: ButtonsDependencies): CommandButtonComponentProps[] => { + if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { + // All the following buttons require write access + return []; + } + + const buttons: CommandButtonComponentProps[] = []; + const label = !isPreferredApiMongoDB ? "New Item" : "New Document"; + if (getNewDocumentButtonState(editorState).visible) { + buttons.push({ + iconSrc: NewDocumentIcon, + iconAlt: label, + keyboardAction: KeyboardAction.NEW_ITEM, + onCommandClick: onNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: + !getNewDocumentButtonState(editorState).enabled || + useSelectedNode.getState().isQueryCopilotCollectionSelected(), + id: NEW_DOCUMENT_BUTTON_ID, + }); + } + + if (getSaveNewDocumentButtonState(editorState).visible) { + const label = "Save"; + buttons.push({ + iconSrc: SaveIcon, + iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, + onCommandClick: onSaveNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: + !getSaveNewDocumentButtonState(editorState).enabled || + useSelectedNode.getState().isQueryCopilotCollectionSelected(), + id: SAVE_BUTTON_ID, + }); + } + + if (getDiscardNewDocumentChangesButtonState(editorState).visible) { + const label = "Discard"; + buttons.push({ + iconSrc: DiscardIcon, + iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, + onCommandClick: onRevertNewDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: + !getDiscardNewDocumentChangesButtonState(editorState).enabled || + useSelectedNode.getState().isQueryCopilotCollectionSelected(), + id: DISCARD_BUTTON_ID, + }); + } + + if (getSaveExistingDocumentButtonState(editorState).visible) { + const label = "Update"; + buttons.push({ + iconSrc: SaveIcon, + iconAlt: label, + keyboardAction: KeyboardAction.SAVE_ITEM, + onCommandClick: onSaveExistingDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: + !getSaveExistingDocumentButtonState(editorState).enabled || + useSelectedNode.getState().isQueryCopilotCollectionSelected(), + id: UPDATE_BUTTON_ID, + }); + } + + if (getDiscardExistingDocumentChangesButtonState(editorState).visible) { + const label = "Discard"; + buttons.push({ + iconSrc: DiscardIcon, + iconAlt: label, + keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, + onCommandClick: onRevertExistingDocumentClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: + !getDiscardExistingDocumentChangesButtonState(editorState).enabled || + useSelectedNode.getState().isQueryCopilotCollectionSelected(), + id: DISCARD_BUTTON_ID, + }); + } + + if (selectedRows.size > 0) { + const label = "Delete"; + buttons.push({ + iconSrc: DeleteDocumentIcon, + iconAlt: label, + keyboardAction: KeyboardAction.DELETE_ITEM, + onCommandClick: onDeleteExistingDocumentsClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), + id: DELETE_BUTTON_ID, + }); + } + + if (!isPreferredApiMongoDB) { + buttons.push(createUploadButton(_collection.container)); + } + + return buttons; +}; + +const updateNavbarWithTabsButtons = (isTabActive: boolean, dependencies: ButtonsDependencies): void => { + if (isTabActive) { + useCommandBar.getState().setContextButtons(getTabsButtons(dependencies)); + } +}; + +const getNewDocumentButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ + enabled: (() => { + switch (editorState) { + case ViewModels.DocumentExplorerState.noDocumentSelected: + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + return true; + default: + return false; + } + })(), + visible: true, +}); + +const _loadNextPageInternal = ( + iterator: QueryIterator, +): Promise => { + return iterator.fetchNext().then((response) => response.resources); +}; + +// Export to expose to unit tests +export const showPartitionKey = (collection: ViewModels.CollectionBase, isPreferredApiMongoDB: boolean) => { + if (!collection) { + return false; + } + + if (!collection.partitionKey) { + return false; + } + + if (collection.partitionKey.systemKey && isPreferredApiMongoDB) { + return false; + } + + return true; +}; + +// Export to expose to unit tests +export const buildQuery = ( + isMongo: boolean, + filter: string, + partitionKeyProperties?: string[], + partitionKey?: DataModels.PartitionKey, +): string => { + if (isMongo) { + return filter || "{}"; + } + + return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey); +}; + +// Export to expose to unit tests +export interface IDocumentsTabComponentProps { + isPreferredApiMongoDB: boolean; + documentIds: DocumentId[]; // TODO: this contains ko observables. We need to convert them to React state. + collection: ViewModels.CollectionBase; + partitionKey: DataModels.PartitionKey; + onLoadStartKey: number; + tabTitle: string; + resourceTokenPartitionKey?: string; + onExecutionErrorChange: (isExecutionError: boolean) => void; + onIsExecutingChange: (isExecuting: boolean) => void; + isTabActive: boolean; +} + +// Export to expose to unit tests +export const DocumentsTabComponent: React.FunctionComponent = ({ + isPreferredApiMongoDB, + documentIds: _documentIds, + collection: _collection, + partitionKey: _partitionKey, + onLoadStartKey: _onLoadStartKey, + tabTitle, + resourceTokenPartitionKey, + onExecutionErrorChange, + onIsExecutingChange, + isTabActive, +}): JSX.Element => { + const [isFilterCreated, setIsFilterCreated] = useState(true); + const [isFilterExpanded, setIsFilterExpanded] = useState(false); + const [isFilterFocused, setIsFilterFocused] = useState(false); + const [appliedFilter, setAppliedFilter] = useState(""); + const [filterContent, setFilterContent] = useState(""); + const [documentIds, setDocumentIds] = useState([]); + const [isExecuting, setIsExecuting] = useState(false); + const filterInput = useRef(null); + + // Query + const [documentsIterator, setDocumentsIterator] = useState<{ + iterator: QueryIterator; + applyFilterButtonPressed: boolean; + }>(undefined); + const [queryAbortController, setQueryAbortController] = useState(undefined); + const [cancelQueryTimeoutID, setCancelQueryTimeoutID] = useState(undefined); + + const [onLoadStartKey, setOnLoadStartKey] = useState(_onLoadStartKey); + + const [initialDocumentContent, setInitialDocumentContent] = useState(undefined); + const [selectedDocumentContent, setSelectedDocumentContent] = useState(undefined); + const [selectedDocumentContentBaseline, setSelectedDocumentContentBaseline] = useState(undefined); + + // Table user clicked on this row + const [clickedRow, setClickedRow] = useState(undefined); + // Table multiple selection + const [selectedRows, setSelectedRows] = React.useState>(() => new Set([0])); + + // Command buttons + const [editorState, setEditorState] = useState( + ViewModels.DocumentExplorerState.noDocumentSelected, + ); + + const isQueryCopilotSampleContainer = + _collection?.isSampleCollection && + _collection?.databaseId === QueryCopilotSampleDatabaseId && + _collection?.id() === QueryCopilotSampleContainerId; + + // For Mongo only + const [continuationToken, setContinuationToken] = useState(undefined); + + const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); + + useEffect(() => { + if (isFilterFocused) { + filterInput.current?.focus(); + } + }, [isFilterFocused]); + + let lastFilterContents = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC']; + + const applyFilterButton = { + enabled: true, + visible: true, + }; + + const partitionKey: DataModels.PartitionKey = useMemo( + () => _partitionKey || (_collection && _collection.partitionKey), + [_collection, _partitionKey], + ); + const partitionKeyPropertyHeaders: string[] = useMemo( + () => _collection?.partitionKeyPropertyHeaders || partitionKey?.paths, + [_collection?.partitionKeyPropertyHeaders, partitionKey?.paths], + ); + let partitionKeyProperties = useMemo( + () => + partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => + partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), + ), + [partitionKeyPropertyHeaders], + ); + + // new DocumentId() requires a DocumentTab which we mock with only the required properties + const newDocumentId = useCallback( + (rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) => + new DocumentId( + { + partitionKey, + partitionKeyProperties, + // Fake unused mocks + isEditorDirty: () => false, + selectDocument: () => Promise.reject(), + }, + rawDocument, + partitionKeyValue, + ), + [partitionKey], + ); + + useEffect(() => { + setDocumentIds(_documentIds); + }, [_documentIds]); + + // This is executed in onActivate() in the original code. + useEffect(() => { + setKeyboardActions({ + [KeyboardAction.SEARCH]: () => { + onShowFilterClick(); + return true; + }, + [KeyboardAction.CLEAR_SEARCH]: () => { + setFilterContent(""); + refreshDocumentsGrid(true); + return true; + }, + }); + + if (!documentsIterator) { + try { + refreshDocumentsGrid(); + } catch (error) { + if (onLoadStartKey !== null && onLoadStartKey !== undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + onLoadStartKey, + ); + setOnLoadStartKey(undefined); + } + } + } + + updateNavbarWithTabsButtons(isTabActive, { + _collection, + selectedRows, + editorState, + isPreferredApiMongoDB, + onNewDocumentClick, + onSaveNewDocumentClick, + onRevertNewDocumentClick, + onSaveExistingDocumentClick, + onRevertExistingDocumentClick, + onDeleteExistingDocumentsClick, + }); + }, []); + + const isEditorDirty = useCallback((): boolean => { + switch (editorState) { + case ViewModels.DocumentExplorerState.noDocumentSelected: + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + return false; + + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + return true; + + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + + default: + return false; + } + }, [editorState]); + + const confirmDiscardingChange = useCallback( + (onDiscard: () => void, onCancelDiscard?: () => void): void => { + if (isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Your unsaved changes will be lost. Do you want to continue?", + "OK", + onDiscard, + "Cancel", + onCancelDiscard, + ); + } else { + onDiscard(); + } + }, + [isEditorDirty], + ); + + // Update parent (tab) if isExecuting has changed + useEffect(() => { + onIsExecutingChange(isExecuting); + }, [onIsExecutingChange, isExecuting]); + + const onNewDocumentClick = useCallback( + (): void => confirmDiscardingChange(() => initializeNewDocument()), + [confirmDiscardingChange], + ); + + const initializeNewDocument = (): void => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const newDocument: any = { + id: "replace_with_new_document_id", + }; + partitionKeyProperties.forEach((partitionKeyProperty) => { + let target = newDocument; + const keySegments = partitionKeyProperty.split("."); + const finalSegment = keySegments.pop(); + + // Initialize nested objects as needed + keySegments.forEach((segment) => { + target = target[segment] = target[segment] || {}; + }); + + target[finalSegment] = "replace_with_new_partition_key_value"; + }); + const defaultDocument: string = renderObjectForEditor(newDocument, null, 4); + + setInitialDocumentContent(defaultDocument); + setSelectedDocumentContent(defaultDocument); + setSelectedDocumentContentBaseline(defaultDocument); + setSelectedRows(new Set()); + setClickedRow(undefined); + setEditorState(ViewModels.DocumentExplorerState.newDocumentValid); + }; + + let onSaveNewDocumentClick = useCallback((): Promise => { + onExecutionErrorChange(false); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }); + const sanitizedContent = selectedDocumentContent.replace("\n", ""); + const document = JSON.parse(sanitizedContent); + setIsExecuting(true); + return createDocument(_collection, document) + .then( + (savedDocument: DataModels.DocumentId) => { + const value: string = renderObjectForEditor(savedDocument || {}, null, 4); + setSelectedDocumentContentBaseline(value); + setInitialDocumentContent(value); + const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( + savedDocument, + partitionKey as PartitionKeyDefinition, + ); + const id = newDocumentId(savedDocument, partitionKeyProperties, partitionKeyValueArray as string[]); + const ids = documentIds; + ids.push(id); + + setDocumentIds(ids); + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + TelemetryProcessor.traceSuccess( + Action.CreateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + }, + (error) => { + onExecutionErrorChange(true); + const errorMessage = getErrorMessage(error); + useDialog.getState().showOkModalDialog("Create document failed", errorMessage); + TelemetryProcessor.traceFailure( + Action.CreateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey, + ); + }, + ) + .then(() => setSelectedRows(new Set([documentIds.length - 1]))) + .finally(() => setIsExecuting(false)); + }, [ + onExecutionErrorChange, + tabTitle, + selectedDocumentContent, + _collection, + partitionKey, + newDocumentId, + partitionKeyProperties, + documentIds, + ]); + + const onRevertNewDocumentClick = useCallback((): void => { + setInitialDocumentContent(""); + setSelectedDocumentContent(""); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + }, [setInitialDocumentContent, setSelectedDocumentContent, setEditorState]); + + let onSaveExistingDocumentClick = useCallback((): Promise => { + const documentContent = JSON.parse(selectedDocumentContent); + + const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( + documentContent, + partitionKey as PartitionKeyDefinition, + ); + + const selectedDocumentId = documentIds[clickedRow as number]; + selectedDocumentId.partitionKeyValue = partitionKeyValueArray; + + onExecutionErrorChange(false); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }); + setIsExecuting(true); + return updateDocument(_collection, selectedDocumentId, documentContent) + .then( + (updatedDocument: Item & { _rid: string }) => { + const value: string = renderObjectForEditor(updatedDocument || {}, null, 4); + setSelectedDocumentContentBaseline(value); + setInitialDocumentContent(value); + setSelectedDocumentContent(value); + documentIds.forEach((documentId: DocumentId) => { + if (documentId.rid === updatedDocument._rid) { + documentId.id(updatedDocument.id); + } + }); + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + TelemetryProcessor.traceSuccess( + Action.UpdateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + }, + (error) => { + onExecutionErrorChange(true); + const errorMessage = getErrorMessage(error); + useDialog.getState().showOkModalDialog("Update document failed", errorMessage); + TelemetryProcessor.traceFailure( + Action.UpdateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey, + ); + }, + ) + .finally(() => setIsExecuting(false)); + }, [onExecutionErrorChange, tabTitle, selectedDocumentContent, _collection, partitionKey, documentIds, clickedRow]); + + const onRevertExistingDocumentClick = useCallback((): void => { + setSelectedDocumentContentBaseline(initialDocumentContent); + setSelectedDocumentContent(selectedDocumentContentBaseline); + }, [initialDocumentContent, selectedDocumentContentBaseline, setSelectedDocumentContent]); + + /** + * Implementation using bulk delete + */ + let _deleteDocuments = useCallback( + async (toDeleteDocumentIds: DocumentId[]): Promise => { + onExecutionErrorChange(false); + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocuments, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }); + setIsExecuting(true); + return deleteNoSqlDocuments(_collection, toDeleteDocumentIds) + .then( + (deletedIds) => { + TelemetryProcessor.traceSuccess( + Action.DeleteDocuments, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + return deletedIds; + }, + (error) => { + onExecutionErrorChange(true); + console.error(error); + TelemetryProcessor.traceFailure( + Action.DeleteDocuments, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey, + ); + throw error; + }, + ) + .finally(() => setIsExecuting(false)); + }, + [_collection, onExecutionErrorChange, tabTitle], + ); + + const deleteDocuments = useCallback( + (toDeleteDocumentIds: DocumentId[]): void => { + onExecutionErrorChange(false); + setIsExecuting(true); + _deleteDocuments(toDeleteDocumentIds) + .then( + (deletedIds: DocumentId[]) => { + const deletedRids = new Set(deletedIds.map((documentId) => documentId.rid)); + const newDocumentIds = [...documentIds.filter((documentId) => !deletedRids.has(documentId.rid))]; + setDocumentIds(newDocumentIds); + + setSelectedDocumentContent(undefined); + setClickedRow(undefined); + setSelectedRows(new Set()); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + useDialog + .getState() + .showOkModalDialog("Delete documents", `${deletedIds.length} document(s) successfully deleted.`); + }, + (error: Error) => + useDialog + .getState() + .showOkModalDialog("Delete documents", `Document(s) deleted failed (${JSON.stringify(error)})`), + ) + .finally(() => setIsExecuting(false)); + }, + [onExecutionErrorChange, _deleteDocuments, documentIds], + ); + + const onDeleteExistingDocumentsClick = useCallback(async (): Promise => { + // TODO: Rework this for localization + const isPlural = selectedRows.size > 1; + const documentName = !isPreferredApiMongoDB + ? isPlural + ? `the selected ${selectedRows.size} items` + : "the selected item" + : isPlural + ? `the selected ${selectedRows.size} documents` + : "the selected document"; + const msg = `Are you sure you want to delete ${documentName}?`; + + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + msg, + "Delete", + () => deleteDocuments(Array.from(selectedRows).map((index) => documentIds[index as number])), + "Cancel", + undefined, + ); + }, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]); + + // If editor state changes, update the nav + useEffect( + () => + updateNavbarWithTabsButtons(isTabActive, { + _collection, + selectedRows, + editorState, + isPreferredApiMongoDB, + onNewDocumentClick, + onSaveNewDocumentClick, + onRevertNewDocumentClick, + onSaveExistingDocumentClick, + onRevertExistingDocumentClick: onRevertExistingDocumentClick, + onDeleteExistingDocumentsClick: onDeleteExistingDocumentsClick, + }), + [ + _collection, + selectedRows, + editorState, + isPreferredApiMongoDB, + onNewDocumentClick, + onSaveNewDocumentClick, + onRevertNewDocumentClick, + onSaveExistingDocumentClick, + onRevertExistingDocumentClick, + onDeleteExistingDocumentsClick, + isTabActive, + ], + ); + + const onShowFilterClick = () => { + setIsFilterCreated(true); + setIsFilterExpanded(true); + setIsFilterFocused(true); + }; + + const queryTimeoutEnabled = useCallback( + (): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), + [isPreferredApiMongoDB], + ); + + const createIterator = useCallback((): QueryIterator => { + const _queryAbortController = new AbortController(); + setQueryAbortController(_queryAbortController); + const filter: string = filterContent.trim(); + const query: string = buildQuery(isPreferredApiMongoDB, filter, partitionKeyProperties, partitionKey); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const options: any = {}; + // TODO: Property 'enableCrossPartitionQuery' does not exist on type 'FeedOptions'. + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + + if (resourceTokenPartitionKey) { + options.partitionKey = resourceTokenPartitionKey; + } + // Fixes compile error error TS2741: Property 'throwIfAborted' is missing in type 'AbortSignal' but required in type 'import("/home/runner/work/cosmos-explorer/cosmos-explorer/node_modules/node-abort-controller/index").AbortSignal'. + options.abortSignal = _queryAbortController.signal; + + return isQueryCopilotSampleContainer + ? querySampleDocuments(query, options) + : queryDocuments(_collection.databaseId, _collection.id(), query, options); + }, [ + filterContent, + isPreferredApiMongoDB, + partitionKeyProperties, + partitionKey, + resourceTokenPartitionKey, + isQueryCopilotSampleContainer, + _collection, + ]); + + const onHideFilterClick = (): void => { + setIsFilterExpanded(false); + }; + + const onCloseButtonKeyDown: KeyboardEventHandler = (event) => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + onHideFilterClick(); + event.stopPropagation(); + return false; + } + return true; + }; + + let loadNextPage = useCallback( + (iterator: QueryIterator, applyFilterButtonClicked?: boolean): Promise => { + setIsExecuting(true); + onExecutionErrorChange(false); + let automaticallyCancelQueryAfterTimeout: boolean; + if (applyFilterButtonClicked && queryTimeoutEnabled()) { + const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout); + automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean( + StorageKey.AutomaticallyCancelQueryAfterTimeout, + ); + const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => { + if (isExecuting) { + if (automaticallyCancelQueryAfterTimeout) { + queryAbortController.abort(); + } else { + useDialog + .getState() + .showOkCancelModalDialog( + QueryConstants.CancelQueryTitle, + format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached), + "Yes", + () => queryAbortController.abort(), + "No", + undefined, + ); + } + } + }, queryTimeout); + setCancelQueryTimeoutID(cancelQueryTimeoutID); + } + return _loadNextPageInternal(iterator) + .then( + (documentsIdsResponse = []) => { + const currentDocuments = documentIds; + const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); + const nextDocumentIds = documentsIdsResponse + // filter documents already loaded in observable + .filter((d: DataModels.DocumentId) => { + return currentDocumentsRids.indexOf(d._rid) < 0; + }) + // map raw response to view model + .map((rawDocument: DataModels.DocumentId & { _partitionKeyValue: string[] }) => { + const partitionKeyValue = rawDocument._partitionKeyValue; + + const partitionKey = _partitionKey || (_collection && _collection.partitionKey); + const partitionKeyPropertyHeaders = _collection?.partitionKeyPropertyHeaders || partitionKey?.paths; + const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => + partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), + ); + + return newDocumentId(rawDocument, partitionKeyProperties, partitionKeyValue); + }); + + const merged = currentDocuments.concat(nextDocumentIds); + setDocumentIds(merged); + if (onLoadStartKey !== null && onLoadStartKey !== undefined) { + TelemetryProcessor.traceSuccess( + Action.Tab, + { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + onLoadStartKey, + ); + setOnLoadStartKey(undefined); + } + }, + (error) => { + onExecutionErrorChange(true); + const errorMessage = getErrorMessage(error); + logConsoleError(errorMessage); + if (onLoadStartKey !== null && onLoadStartKey !== undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: errorMessage, + errorStack: getErrorStack(error), + }, + onLoadStartKey, + ); + setOnLoadStartKey(undefined); + } + }, + ) + .finally(() => { + setIsExecuting(false); + if (applyFilterButtonClicked && queryTimeoutEnabled()) { + clearTimeout(cancelQueryTimeoutID); + if (!automaticallyCancelQueryAfterTimeout) { + useDialog.getState().closeDialog(); + } + } + }); + }, + [ + onExecutionErrorChange, + queryTimeoutEnabled, + isExecuting, + queryAbortController, + documentIds, + onLoadStartKey, + _partitionKey, + _collection, + newDocumentId, + tabTitle, + cancelQueryTimeoutID, + ], + ); + + useEffect(() => { + if (documentsIterator) { + loadNextPage(documentsIterator.iterator, documentsIterator.applyFilterButtonPressed); + } + }, [ + documentsIterator, // loadNextPage: disabled as it will trigger a circular dependency and infinite loop + ]); + + const onRefreshKeyInput: KeyboardEventHandler = (event) => { + if (event.key === " " || event.key === "Enter") { + const focusElement = event.target as HTMLElement; + refreshDocumentsGrid(false); + focusElement && focusElement.focus(); + event.stopPropagation(); + event.preventDefault(); + } + }; + + const onLoadMoreKeyInput: KeyboardEventHandler = (event) => { + if (event.key === " " || event.key === "Enter") { + const focusElement = event.target as HTMLElement; + loadNextPage(documentsIterator.iterator); + focusElement && focusElement.focus(); + event.stopPropagation(); + event.preventDefault(); + } + }; + + const onFilterKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === "Enter") { + refreshDocumentsGrid(true); + + // Suppress the default behavior of the key + e.preventDefault(); + } else if (e.key === "Escape") { + onHideFilterClick(); + + // Suppress the default behavior of the key + e.preventDefault(); + } + }; + + const _isQueryCopilotSampleContainer = + _collection?.isSampleCollection && + _collection?.databaseId === QueryCopilotSampleDatabaseId && + _collection?.id() === QueryCopilotSampleContainerId; + + // Table config here + const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => { + const item: Record & { id: string } = { + id: documentId.id(), + }; + + if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) { + for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) { + item[partitionKeyPropertyHeaders[i]] = documentId.stringPartitionKeyValues[i]; + } + } + + return item; + }); + + /** + * replicate logic of selectedDocument.click(); + * Document has been clicked on in table + * @param tabRowId + */ + const onDocumentClicked = (tabRowId: TableRowId) => { + const index = tabRowId as number; + setClickedRow(index); + loadDocument(documentIds[index]); + }; + + let loadDocument = (documentId: DocumentId) => + (_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then( + (content) => { + initDocumentEditor(documentId, content); + }, + ); + + const initDocumentEditor = (documentId: DocumentId, documentContent: unknown): void => { + if (documentId) { + const content: string = renderObjectForEditor(documentContent, null, 4); + setSelectedDocumentContentBaseline(content); + setSelectedDocumentContent(content); + setInitialDocumentContent(content); + + const newState = documentId + ? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits + : ViewModels.DocumentExplorerState.newDocumentValid; + setEditorState(newState); + } + }; + + const _onEditorContentChange = (newContent: string): void => { + setSelectedDocumentContent(newContent); + + if ( + selectedDocumentContentBaseline === initialDocumentContent && + newContent === initialDocumentContent && + editorState !== ViewModels.DocumentExplorerState.newDocumentValid + ) { + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + return; + } + + // Mongo uses BSON format for _id, trying to parse it as JSON blocks normal flow in an edit + // Bypass validation for mongo + if (isPreferredApiMongoDB) { + onValidDocumentEdit(); + return; + } + + try { + JSON.parse(newContent); + onValidDocumentEdit(); + } catch (e) { + onInvalidDocumentEdit(); + } + }; + + const onValidDocumentEdit = (): void => { + if ( + editorState === ViewModels.DocumentExplorerState.newDocumentInvalid || + editorState === ViewModels.DocumentExplorerState.newDocumentValid + ) { + setEditorState(ViewModels.DocumentExplorerState.newDocumentValid); + return; + } + + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); + }; + + const onInvalidDocumentEdit = (): void => { + if ( + editorState === ViewModels.DocumentExplorerState.newDocumentInvalid || + editorState === ViewModels.DocumentExplorerState.newDocumentValid + ) { + setEditorState(ViewModels.DocumentExplorerState.newDocumentInvalid); + return; + } + + if ( + editorState === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || + editorState === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid + ) { + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); + return; + } + }; + + const tableContainerRef = useRef(null); + const [tableContainerSizePx, setTableContainerSizePx] = useState<{ height: number; width: number }>(undefined); + useEffect(() => { + if (!tableContainerRef.current) { + return undefined; + } + const resizeObserver = new ResizeObserver(() => + setTableContainerSizePx({ + height: tableContainerRef.current.offsetHeight, + width: tableContainerRef.current.offsetWidth, + }), + ); + resizeObserver.observe(tableContainerRef.current); + return () => resizeObserver.disconnect(); // clean up + }, []); + + const columnHeaders = { + idHeader: isPreferredApiMongoDB ? "_id" : "id", + partitionKeyHeaders: (showPartitionKey(_collection, isPreferredApiMongoDB) && partitionKeyPropertyHeaders) || [], + }; + + const onSelectedRowsChange = (selectedRows: Set) => { + confirmDiscardingChange(() => { + if (selectedRows.size === 0) { + setSelectedDocumentContent(undefined); + setClickedRow(undefined); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + } + + // Find if clickedRow is in selectedRows.If not, clear clickedRow and content + if (clickedRow !== undefined && !selectedRows.has(clickedRow)) { + setClickedRow(undefined); + setSelectedDocumentContent(undefined); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + } + + // If only one selection, we consider as a click + if (selectedRows.size === 1) { + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + } + + setSelectedRows(selectedRows); + }); + }; + + // ********* Override here for mongo (from MongoDocumentsTab) ********** + if (isPreferredApiMongoDB) { + loadDocument = (documentId: DocumentId) => + MongoProxyClient.readDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId).then( + (content) => { + initDocumentEditor(documentId, content); + }, + ); + + renderObjectForEditor = (value: unknown): string => MongoUtility.tojson(value, null, false); + + const _hasShardKeySpecified = (document: unknown): boolean => { + return Boolean(extractPartitionKeyValues(document, _getPartitionKeyDefinition() as PartitionKeyDefinition)); + }; + + const _getPartitionKeyDefinition = (): DataModels.PartitionKey => { + let partitionKey: DataModels.PartitionKey = _partitionKey; + + if ( + _partitionKey && + _partitionKey.paths && + _partitionKey.paths.length && + _partitionKey.paths.length > 0 && + _partitionKey.paths[0].indexOf("$v") > -1 + ) { + // Convert BsonSchema2 to /path format + partitionKey = { + kind: partitionKey.kind, + paths: ["/" + partitionKeyProperties?.[0].replace(/\./g, "/")], + version: partitionKey.version, + }; + } + + return partitionKey; + }; + + lastFilterContents = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"]; + partitionKeyProperties = partitionKeyProperties?.map((partitionKeyProperty, i) => { + if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) { + partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, ""); + } + + if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) { + // From $v.shard.$v.key.$v > shard.key + partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); + partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty; + } + + return partitionKeyProperty; + }); + + /** + * Mongo implementation + * TODO: update proxy to use mongo driver deleteMany + */ + _deleteDocuments = (toDeleteDocumentIds: DocumentId[]): Promise => { + const promises = toDeleteDocumentIds.map((documentId) => _deleteDocument(documentId)); + return Promise.all(promises); + }; + + const __deleteDocument = async (documentId: DocumentId): Promise => { + await MongoProxyClient.deleteDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId); + return documentId; + }; + + const _deleteDocument = useCallback( + (documentId: DocumentId): Promise => { + onExecutionErrorChange(false); + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }); + setIsExecuting(true); + return __deleteDocument(documentId) + .then( + (deletedDocumentId) => { + TelemetryProcessor.traceSuccess( + Action.DeleteDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + return deletedDocumentId; + }, + (error) => { + onExecutionErrorChange(true); + console.error(error); + TelemetryProcessor.traceFailure( + Action.DeleteDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey, + ); + return undefined; + }, + ) + .finally(() => setIsExecuting(false)); + }, + [__deleteDocument, onExecutionErrorChange, tabTitle], + ); + + onSaveNewDocumentClick = useCallback((): Promise => { + const documentContent = JSON.parse(selectedDocumentContent); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }); + + const partitionKeyProperty = partitionKeyProperties?.[0]; + if (partitionKeyProperty !== "_id" && !_hasShardKeySpecified(documentContent)) { + const message = `The document is lacking the shard property: ${partitionKeyProperty}`; + useDialog.getState().showOkModalDialog("Create document failed", message); + onExecutionErrorChange(true); + TelemetryProcessor.traceFailure( + Action.CreateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: message, + }, + startKey, + ); + Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); + throw new Error("Document without shard key"); + } + + onExecutionErrorChange(false); + setIsExecuting(true); + return MongoProxyClient.createDocument( + _collection.databaseId, + _collection as ViewModels.Collection, + partitionKeyProperties?.[0], + documentContent, + ) + .then( + (savedDocument: { _self: unknown }) => { + const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( + savedDocument, + _getPartitionKeyDefinition() as PartitionKeyDefinition, + ); + + const id = new ObjectId(this, savedDocument, partitionKeyArray); + const ids = documentIds; + ids.push(id); + delete savedDocument._self; + + const value: string = renderObjectForEditor(savedDocument || {}, null, 4); + setSelectedDocumentContentBaseline(value); + + setDocumentIds(ids); + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + TelemetryProcessor.traceSuccess( + Action.CreateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + }, + (error) => { + onExecutionErrorChange(true); + const errorMessage = getErrorMessage(error); + useDialog.getState().showOkModalDialog("Create document failed", errorMessage); + TelemetryProcessor.traceFailure( + Action.CreateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey, + ); + }, + ) + .then(() => setSelectedRows(new Set([documentIds.length - 1]))) + .finally(() => setIsExecuting(false)); + }, [ + selectedDocumentContent, + tabTitle, + partitionKeyProperties, + _hasShardKeySpecified, + onExecutionErrorChange, + _collection, + _getPartitionKeyDefinition, + documentIds, + ]); + + onSaveExistingDocumentClick = (): Promise => { + const documentContent = selectedDocumentContent; + onExecutionErrorChange(false); + setIsExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }); + + const selectedDocumentId = documentIds[clickedRow as number]; + return MongoProxyClient.updateDocument( + _collection.databaseId, + _collection as ViewModels.Collection, + selectedDocumentId, + documentContent, + ) + .then( + (updatedDocument: { _rid: string }) => { + const value: string = renderObjectForEditor(updatedDocument || {}, null, 4); + setSelectedDocumentContentBaseline(value); + + documentIds.forEach((documentId: DocumentId) => { + if (documentId.rid === updatedDocument._rid) { + const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( + updatedDocument, + _getPartitionKeyDefinition() as PartitionKeyDefinition, + ); + + const id = new ObjectId(this, updatedDocument, partitionKeyArray); + documentId.id(id.id()); + } + }); + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + TelemetryProcessor.traceSuccess( + Action.UpdateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + }, + (error) => { + onExecutionErrorChange(true); + const errorMessage = getErrorMessage(error); + useDialog.getState().showOkModalDialog("Update document failed", errorMessage); + TelemetryProcessor.traceFailure( + Action.UpdateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: errorMessage, + errorStack: getErrorStack(error), + }, + startKey, + ); + }, + ) + .finally(() => setIsExecuting(false)); + }; + + loadNextPage = (): Promise => { + setIsExecuting(true); + onExecutionErrorChange(false); + const filter: string = filterContent.trim(); + const query: string = buildQuery(isPreferredApiMongoDB, filter); + + return MongoProxyClient.queryDocuments( + _collection.databaseId, + _collection as ViewModels.Collection, + true, + query, + continuationToken, + ) + .then( + ({ continuationToken: newContinuationToken, documents }) => { + setContinuationToken(newContinuationToken); + let currentDocuments = documentIds; + const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); + const nextDocumentIds = documents + .filter((d: { _rid: string }) => { + return currentDocumentsRids.indexOf(d._rid) < 0; + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((rawDocument: any) => { + const partitionKeyValue = rawDocument._partitionKeyValue; + return newDocumentId(rawDocument, partitionKeyProperties, [partitionKeyValue]); + // return new DocumentId(this, rawDocument, [partitionKeyValue]); + }); + + const merged = currentDocuments.concat(nextDocumentIds); + + setDocumentIds(merged); + currentDocuments = merged; + + if (filterContent.length > 0 && currentDocuments.length > 0) { + currentDocuments[0].click(); + } else { + setSelectedDocumentContent(""); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + } + if (_onLoadStartKey !== null && _onLoadStartKey !== undefined) { + TelemetryProcessor.traceSuccess( + Action.Tab, + { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + _onLoadStartKey, + ); + setOnLoadStartKey(undefined); + } + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + if (onLoadStartKey !== null && onLoadStartKey !== undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + _onLoadStartKey, + ); + setOnLoadStartKey(undefined); + } + }, + ) + .finally(() => setIsExecuting(false)); + }; + } + // ***************** Mongo *************************** + + const refreshDocumentsGrid = useCallback( + async (applyFilterButtonPressed?: boolean): Promise => { + // clear documents grid + setDocumentIds([]); + try { + // reset iterator which will autoload documents (in useEffect) + setDocumentsIterator({ + iterator: createIterator(), + applyFilterButtonPressed, + }); + + // collapse filter + setAppliedFilter(filterContent); + setIsFilterExpanded(false); + } catch (error) { + console.error(error); + useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); + } + }, + [createIterator, filterContent], + ); + + return ( + +

+ {isFilterCreated && ( +
+ {!isFilterExpanded && !isPreferredApiMongoDB && ( +
+ SELECT * FROM c + {appliedFilter} + +
+ )} + {!isFilterExpanded && isPreferredApiMongoDB && ( +
+ {appliedFilter.length > 0 && Filter :} + {!(appliedFilter.length > 0) && No filter applied} + {appliedFilter} + +
+ )} + {isFilterExpanded && ( +
+
+
+ {!isPreferredApiMongoDB && SELECT * FROM c } + setFilterContent(e.target.value)} + onBlur={() => setIsFilterFocused(false)} + /> + + + {lastFilterContents.map((filter) => ( + + + + + + + {!isPreferredApiMongoDB && isExecuting && ( + + )} + +
+
+
+ )} +
+ )} + {/* doesn't like to be a flex child */} +
+ +
+
+
+ {isTabActive && selectedDocumentContent && selectedRows.size <= 1 && ( + + )} + {selectedRows.size > 1 && ( + Number of selected documents: {selectedRows.size} + )} +
+
+
+
+ + ); +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx new file mode 100644 index 000000000..79d5e2343 --- /dev/null +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx @@ -0,0 +1,195 @@ +import { deleteDocument } from "Common/MongoProxyClient"; +import { Platform, updateConfigContext } from "ConfigContext"; +import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact"; +import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { + DELETE_BUTTON_ID, + DISCARD_BUTTON_ID, + DocumentsTabComponent, + IDocumentsTabComponentProps, + NEW_DOCUMENT_BUTTON_ID, + SAVE_BUTTON_ID, + UPDATE_BUTTON_ID, + buildQuery, +} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; +import { ReactWrapper, ShallowWrapper, mount } from "enzyme"; +import * as ko from "knockout"; +import React from "react"; +import { act } from "react-dom/test-utils"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import Explorer from "../../Explorer"; + +jest.requireActual("Explorer/Controls/Editor/EditorReact"); + +const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__"; + +jest.mock("Common/MongoProxyClient", () => ({ + queryDocuments: jest.fn(() => + Promise.resolve({ + continuationToken: "", + documents: [ + { + _rid: "_rid", + _self: "_self", + _etag: "etag", + _ts: 1234, + id: "id", + }, + ], + headers: {}, + }), + ), + readDocument: jest.fn(() => + Promise.resolve({ + _rid: "_rid1", + _self: "_self1", + _etag: "etag1", + property: PROPERTY_VALUE, + _ts: 5678, + id: "id1", + }), + ), + deleteDocument: jest.fn(() => Promise.resolve()), +})); + +jest.mock("Explorer/Controls/Editor/EditorReact", () => ({ + EditorReact: (props: EditorReactProps) => <>{props.content}, +})); + +jest.mock("Explorer/Controls/Dialog", () => ({ + useDialog: { + getState: jest.fn(() => ({ + showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(), + showOkModalDialog: () => {}, + })), + }, +})); + +async function waitForComponentToPaint

(wrapper: ReactWrapper

| ShallowWrapper

, amount = 0) { + let newWrapper; + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, amount)); + newWrapper = wrapper.update(); + }); + return newWrapper; +} + +describe("Documents tab (Mongo API)", () => { + describe("buildQuery", () => { + it("should generate the right select query for SQL API", () => { + expect(buildQuery(true, "")).toContain("{}"); + }); + }); + + describe("Command bar buttons", () => { + const createMockProps = (): IDocumentsTabComponentProps => ({ + isPreferredApiMongoDB: true, + documentIds: [], + collection: { + id: ko.observable("foo"), + container: new Explorer(), + partitionKey: { + kind: "Hash", + paths: ["/pkey"], + version: 2, + }, + partitionKeyProperties: ["pkey"], + partitionKeyPropertyHeaders: ["/pkey"], + databaseId: "databaseId", + self: "self", + rawDataModel: undefined, + selectedSubnodeKind: undefined, + children: undefined, + isCollectionExpanded: undefined, + onDocumentDBDocumentsClick: (): void => { + throw new Error("Function not implemented."); + }, + onNewQueryClick: (): void => { + throw new Error("Function not implemented."); + }, + expandCollection: (): void => { + throw new Error("Function not implemented."); + }, + collapseCollection: (): void => { + throw new Error("Function not implemented."); + }, + getDatabase: (): ViewModels.Database => { + throw new Error("Function not implemented."); + }, + nodeKind: "nodeKind", + rid: "rid", + }, + partitionKey: undefined, + onLoadStartKey: 0, + tabTitle: "", + onExecutionErrorChange: (isExecutionError: boolean): void => { + isExecutionError; + }, + onIsExecutingChange: (isExecuting: boolean): void => { + isExecuting; + }, + isTabActive: true, + }); + + let wrapper: ReactWrapper; + + beforeEach(async () => { + updateConfigContext({ platform: Platform.Hosted }); + + const props: IDocumentsTabComponentProps = createMockProps(); + + wrapper = mount(); + wrapper = await waitForComponentToPaint(wrapper); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + it("renders by default the first document", async () => { + expect(wrapper.findWhere((node) => node.text().includes(PROPERTY_VALUE)).exists()).toBeTruthy(); + }); + + it("default buttons", async () => { + expect(useCommandBar.getState().contextButtons.find((button) => button.id === UPDATE_BUTTON_ID)).toBeDefined(); + expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined(); + expect(useCommandBar.getState().contextButtons.find((button) => button.id === DELETE_BUTTON_ID)).toBeDefined(); + }); + + it("clicking on New Document should show editor with new document", () => { + act(() => { + useCommandBar + .getState() + .contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID) + .onCommandClick(undefined); + }); + expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy(); + }); + + it("clicking on New Document should show Save and Discard buttons", () => { + act(() => { + useCommandBar + .getState() + .contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID) + .onCommandClick(undefined); + }); + + expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined(); + expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined(); + }); + + it("clicking Delete Document asks for confirmation", () => { + const mockDeleteDocument = deleteDocument as jest.Mock; + mockDeleteDocument.mockClear(); + + act(() => { + useCommandBar + .getState() + .contextButtons.find((button) => button.id === DELETE_BUTTON_ID) + .onCommandClick(undefined); + }); + + expect(mockDeleteDocument).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.test.tsx new file mode 100644 index 000000000..e20084648 --- /dev/null +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.test.tsx @@ -0,0 +1,34 @@ +import { TableRowId } from "@fluentui/react-components"; +import { mount } from "enzyme"; +import React from "react"; +import { DocumentsTableComponent, IDocumentsTableComponentProps } from "./DocumentsTableComponent"; + +const PARTITION_KEY_HEADER = "partitionKey"; +const ID_HEADER = "id"; + +describe("DocumentsTableComponent", () => { + const createMockProps = (): IDocumentsTableComponentProps => ({ + items: [ + { [ID_HEADER]: "1", [PARTITION_KEY_HEADER]: "pk1" }, + { [ID_HEADER]: "2", [PARTITION_KEY_HEADER]: "pk2" }, + { [ID_HEADER]: "3", [PARTITION_KEY_HEADER]: "pk3" }, + ], + onItemClicked: (): void => {}, + onSelectedRowsChange: (): void => {}, + selectedRows: new Set(), + size: { + height: 0, + width: 0, + }, + columnHeaders: { + idHeader: ID_HEADER, + partitionKeyHeaders: [PARTITION_KEY_HEADER], + }, + }); + + it("should render documents and partition keys in header", () => { + const props: IDocumentsTableComponentProps = createMockProps(); + const wrapper = mount(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx new file mode 100644 index 000000000..4089b12bd --- /dev/null +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx @@ -0,0 +1,271 @@ +import { + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + TableRowData as RowStateBase, + Table, + TableBody, + TableCell, + TableCellLayout, + TableColumnDefinition, + TableColumnSizingOptions, + TableHeader, + TableHeaderCell, + TableRow, + TableRowId, + TableSelectionCell, + createTableColumn, + useArrowNavigationGroup, + useTableColumnSizing_unstable, + useTableFeatures, + useTableSelection, +} from "@fluentui/react-components"; +import React, { useCallback, useEffect, useMemo } from "react"; +import { FixedSizeList as List, ListChildComponentProps } from "react-window"; + +export type DocumentsTableComponentItem = { + id: string; +} & Record; + +export type ColumnHeaders = { + idHeader: string; + partitionKeyHeaders: string[]; +}; +export interface IDocumentsTableComponentProps { + items: DocumentsTableComponentItem[]; + onItemClicked: (index: number) => void; + onSelectedRowsChange: (selectedItemsIndices: Set) => void; + selectedRows: Set; + size: { height: number; width: number }; + columnHeaders: ColumnHeaders; + style?: React.CSSProperties; +} + +interface TableRowData extends RowStateBase { + onClick: (e: React.MouseEvent) => void; + onKeyDown: (e: React.KeyboardEvent) => void; + selected: boolean; + appearance: "brand" | "none"; +} +interface ReactWindowRenderFnProps extends ListChildComponentProps { + data: TableRowData[]; +} + +export const DocumentsTableComponent: React.FC = ({ + items, + onItemClicked, + onSelectedRowsChange, + selectedRows, + style, + size, + columnHeaders, +}: IDocumentsTableComponentProps) => { + const [activeItemIndex, setActiveItemIndex] = React.useState(undefined); + + const initialSizingOptions: TableColumnSizingOptions = { + id: { + idealWidth: 280, + minWidth: 50, + }, + }; + columnHeaders.partitionKeyHeaders.forEach((pkHeader) => { + initialSizingOptions[pkHeader] = { + idealWidth: 200, + minWidth: 50, + }; + }); + + const [columnSizingOptions, setColumnSizingOptions] = React.useState(initialSizingOptions); + + const onColumnResize = React.useCallback((_, { columnId, width }) => { + setColumnSizingOptions((state) => ({ + ...state, + [columnId]: { + ...state[columnId], + idealWidth: width, + }, + })); + }, []); + + // Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes + const columns: TableColumnDefinition[] = useMemo( + () => + [ + createTableColumn({ + columnId: "id", + compare: (a, b) => a.id.localeCompare(b.id), + renderHeaderCell: () => columnHeaders.idHeader, + renderCell: (item) => ( + + {item.id} + + ), + }), + ].concat( + columnHeaders.partitionKeyHeaders.map((pkHeader) => + createTableColumn({ + columnId: pkHeader, + compare: (a, b) => a[pkHeader].localeCompare(b[pkHeader]), + // Show Refresh button on last column + renderHeaderCell: () => {pkHeader}, + renderCell: (item) => ( + + {item[pkHeader]} + + ), + }), + ), + ), + [columnHeaders], + ); + + const onIdClicked = useCallback((index: number) => onSelectedRowsChange(new Set([index])), [onSelectedRowsChange]); + + const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => { + const { item, selected, appearance, onClick, onKeyDown } = data[index]; + return ( + + + {columns.map((column) => ( + onSelectedRowsChange(new Set([index]))} + onKeyDown={() => onIdClicked(index)} + {...columnSizing.getTableCellProps(column.columnId)} + tabIndex={column.columnId === "id" ? 0 : -1} + > + {column.renderCell(item)} + + ))} + + ); + }; + + const { + getRows, + columnSizing_unstable: columnSizing, + tableRef, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, + } = useTableFeatures( + { + columns, + items, + }, + [ + useTableColumnSizing_unstable({ columnSizingOptions, onColumnResize }), + useTableSelection({ + selectionMode: "multiselect", + selectedItems: selectedRows, + // eslint-disable-next-line react/prop-types + onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems), + }), + ], + ); + + const rows: TableRowData[] = getRows((row) => { + const selected = isRowSelected(row.rowId); + return { + ...row, + onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === " ") { + e.preventDefault(); + toggleRow(e, row.rowId); + } + }, + selected, + appearance: selected ? ("brand" as const) : ("none" as const), + }; + }); + + const toggleAllKeydown = React.useCallback( + (e: React.KeyboardEvent) => { + if (e.key === " ") { + toggleAllRows(e); + e.preventDefault(); + } + }, + [toggleAllRows], + ); + + // Load document depending on selection + useEffect(() => { + if (selectedRows.size === 1 && items.length > 0) { + const newActiveItemIndex = selectedRows.values().next().value; + if (newActiveItemIndex !== activeItemIndex) { + onItemClicked(newActiveItemIndex); + setActiveItemIndex(newActiveItemIndex); + } + } + }, [selectedRows, items]); + + // Cell keyboard navigation + const keyboardNavAttr = useArrowNavigationGroup({ axis: "grid" }); + + // TODO: Bug in fluent UI typings that requires any here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tableProps: any = { + "aria-label": "Filtered documents table", + role: "grid", + ...columnSizing.getTableProps(), + ...keyboardNavAttr, + size: "extra-small", + ref: tableRef, + ...style, + }; + + return ( + + + + + {columns.map((column /* index */) => ( + + + + {column.renderHeaderCell()} + + + + + + Keyboard Column Resizing + + + + + ))} + + + + + {RenderRow} + + +
+ ); +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap new file mode 100644 index 000000000..0b4c7578e --- /dev/null +++ b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap @@ -0,0 +1,558 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Documents tab (noSql API) when rendered should render the page 1`] = ` + +

+
+
+ + SELECT * FROM c + + + +
+
+
+ +
+
+
+ +
+
+ +`; diff --git a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTableComponent.test.tsx.snap b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTableComponent.test.tsx.snap new file mode 100644 index 000000000..6e80eac9f --- /dev/null +++ b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTableComponent.test.tsx.snap @@ -0,0 +1,1140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DocumentsTableComponent should render documents and partition keys in header 1`] = ` + + +
+ +
+ +
+ +
+ + + +
+ + +
+ + + + + + , + }, + } + } + > + + + } + className="documentsTableCell" + data-tabster="{\\"restorer\\":{\\"type\\":1}}" + id="menu3" + key="id" + onContextMenu={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + onMouseMove={[Function]} + style={ + Object { + "maxWidth": 50, + "minWidth": 50, + "width": 50, + } + } + > + + + + +
, + }, + } + } + > + + + + + + + +
+
+
+
+ +
+ +
+
+ + +
+ +
+ + + +
+ + +
+ + +
+ +
+
+ + 1 + +
+
+
+
+
+ +
+ +
+
+ + pk1 + +
+
+
+
+
+
+ + + + +
+ +
+ + + +
+ + +
+ + +
+ +
+
+ + 2 + +
+
+
+
+
+ +
+ +
+
+ + pk2 + +
+
+
+
+
+
+ + + + +
+ +
+ + + +
+ + +
+ + +
+ +
+
+ + 3 + +
+
+
+
+
+ +
+ +
+
+ + pk3 + +
+
+
+
+
+
+ + +
+
+ +
+ +
+
+
+`; diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts deleted file mode 100644 index d3bf31a54..000000000 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; -import { extractPartitionKeyValues } from "Utils/QueryUtils"; -import * as ko from "knockout"; -import Q from "q"; -import * as Constants from "../../Common/Constants"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as Logger from "../../Common/Logger"; -import { - createDocument, - deleteDocument, - queryDocuments, - readDocument, - updateDocument, -} from "../../Common/MongoProxyClient"; -import MongoUtility from "../../Common/MongoUtility"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { useDialog } from "../Controls/Dialog"; -import DocumentId from "../Tree/DocumentId"; -import ObjectId from "../Tree/ObjectId"; -import DocumentsTab from "./DocumentsTab"; - -export default class MongoDocumentsTab extends DocumentsTab { - public collection: ViewModels.Collection; - private continuationToken: string; - - constructor(options: ViewModels.DocumentsTabOptions) { - super(options); - this.lastFilterContents = ko.observableArray(['{"id":"foo"}', "{ qty: { $gte: 20 } }"]); - - this.partitionKeyProperties = this.partitionKeyProperties?.map((partitionKeyProperty, i) => { - if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) { - partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, ""); - } - - if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) { - // From $v.shard.$v.key.$v > shard.key - partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); - this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty; - } - - return partitionKeyProperty; - }); - - this.isFilterExpanded = ko.observable(true); - super.buildCommandBarOptions.bind(this); - super.buildCommandBarOptions(); - } - - public onSaveNewDocumentClick = (): Promise => { - const documentContent = JSON.parse(this.selectedDocumentContent()); - this.displayedError(""); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - const partitionKeyProperty = this.partitionKeyProperties?.[0]; - if (partitionKeyProperty !== "_id" && !this._hasShardKeySpecified(documentContent)) { - const message = `The document is lacking the shard property: ${partitionKeyProperty}`; - this.displayedError(message); - let that = this; - setTimeout(() => { - that.displayedError(""); - }, Constants.ClientDefaults.errorNotificationTimeoutMs); - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: message, - }, - startKey, - ); - Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); - throw new Error("Document without shard key"); - } - - this.isExecutionError(false); - this.isExecuting(true); - return createDocument( - this.collection.databaseId, - this.collection, - this.partitionKeyProperties?.[0], - documentContent, - ) - .then( - (savedDocument: any) => { - const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( - savedDocument, - this._getPartitionKeyDefinition() as PartitionKeyDefinition, - ); - - let id = new ObjectId(this, savedDocument, partitionKeyArray); - let ids = this.documentIds(); - ids.push(id); - delete savedDocument._self; - - let value: string = this.renderObjectForEditor(savedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - - this.selectedDocumentId(id); - this.documentIds(ids); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - useDialog.getState().showOkModalDialog("Create document failed", errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - }; - - public onSaveExistingDocumentClick = (): Promise => { - const selectedDocumentId = this.selectedDocumentId(); - const documentContent = this.selectedDocumentContent(); - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent) - .then( - (updatedDocument: any) => { - let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); - this.selectedDocumentContent.setBaseline(value); - - this.documentIds().forEach((documentId: DocumentId) => { - if (documentId.rid === updatedDocument._rid) { - const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( - updatedDocument, - this._getPartitionKeyDefinition() as PartitionKeyDefinition, - ); - - const id = new ObjectId(this, updatedDocument, partitionKeyArray); - documentId.id(id.id()); - } - }); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey, - ); - }, - (error) => { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - useDialog.getState().showOkModalDialog("Update document failed", errorMessage); - TelemetryProcessor.traceFailure( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey, - ); - }, - ) - .finally(() => this.isExecuting(false)); - }; - - public buildQuery(filter: string): string { - return filter || "{}"; - } - - public async selectDocument(documentId: DocumentId): Promise { - this.selectedDocumentId(documentId); - const content = await readDocument(this.collection.databaseId, this.collection, documentId); - this.initDocumentEditor(documentId, content); - } - - public loadNextPage(): Q.Promise { - this.isExecuting(true); - this.isExecutionError(false); - const filter: string = this.filterContent().trim(); - const query: string = this.buildQuery(filter); - - return Q(queryDocuments(this.collection.databaseId, this.collection, true, query, this.continuationToken)) - .then( - ({ continuationToken, documents }) => { - this.continuationToken = continuationToken; - let currentDocuments = this.documentIds(); - const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); - const nextDocumentIds = documents - .filter((d: any) => { - return currentDocumentsRids.indexOf(d._rid) < 0; - }) - .map((rawDocument: any) => { - const partitionKeyValue = rawDocument._partitionKeyValue; - return new DocumentId(this, rawDocument, [partitionKeyValue]); - }); - - const merged = currentDocuments.concat(nextDocumentIds); - - this.documentIds(merged); - currentDocuments = this.documentIds(); - if (this.filterContent().length > 0 && currentDocuments.length > 0) { - currentDocuments[0].click(); - } else { - this.selectedDocumentContent(""); - this.selectedDocumentId(null); - this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); - } - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceSuccess( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - }, - (error: any) => { - if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseName: this.collection.databaseId, - collectionName: this.collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - this.onLoadStartKey, - ); - this.onLoadStartKey = null; - } - }, - ) - .finally(() => this.isExecuting(false)); - } - - protected _onEditorContentChange(newContent: string) { - try { - if ( - this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid || - this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid - ) { - let parsed: any = JSON.parse(newContent); - } - - // Mongo uses BSON format for _id, trying to parse it as JSON blocks normal flow in an edit - this.onValidDocumentEdit(); - } catch (e) { - this.onInvalidDocumentEdit(); - } - } - - /** Renders a Javascript object to be displayed inside Monaco Editor */ - public renderObjectForEditor(value: any, replacer: any, space: string | number): string { - return MongoUtility.tojson(value, null, false); - } - - private _hasShardKeySpecified(document: any): boolean { - return Boolean(extractPartitionKeyValues(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition)); - } - - private _getPartitionKeyDefinition(): DataModels.PartitionKey { - let partitionKey: DataModels.PartitionKey = this.partitionKey; - - if ( - this.partitionKey && - this.partitionKey.paths && - this.partitionKey.paths.length && - this.partitionKey.paths.length > 0 && - this.partitionKey.paths[0].indexOf("$v") > -1 - ) { - // Convert BsonSchema2 to /path format - partitionKey = { - kind: partitionKey.kind, - paths: ["/" + this.partitionKeyProperties?.[0].replace(/\./g, "/")], - version: partitionKey.version, - }; - } - - return partitionKey; - } - - protected __deleteDocument(documentId: DocumentId): Promise { - return deleteDocument(this.collection.databaseId, this.collection, documentId); - } -} diff --git a/src/Explorer/Tabs/useTabs.test.ts b/src/Explorer/Tabs/useTabs.test.ts index 90a13f827..9d4925a2c 100644 --- a/src/Explorer/Tabs/useTabs.test.ts +++ b/src/Explorer/Tabs/useTabs.test.ts @@ -1,17 +1,17 @@ +import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; -import { useTabs } from "../../hooks/useTabs"; import { updateUserContext } from "../../UserContext"; +import { useTabs } from "../../hooks/useTabs"; import { container } from "../Controls/Settings/TestUtils"; import DocumentId from "../Tree/DocumentId"; -import DocumentsTab from "./DocumentsTab"; import { NewQueryTab } from "./QueryTab/QueryTab"; describe("useTabs tests", () => { let database: ViewModels.Database; let collection: ViewModels.Collection; let queryTab: NewQueryTab; - let documentsTab: DocumentsTab; + let documentsTab: DocumentsTabV2; beforeEach(() => { updateUserContext({ @@ -56,7 +56,7 @@ describe("useTabs tests", () => { }, ); - documentsTab = new DocumentsTab({ + documentsTab = new DocumentsTabV2({ partitionKey: undefined, documentIds: ko.observableArray(), tabKind: ViewModels.CollectionTabKind.Documents, diff --git a/src/Explorer/Theme/ThemeUtil.ts b/src/Explorer/Theme/ThemeUtil.ts new file mode 100644 index 000000000..fe4075c69 --- /dev/null +++ b/src/Explorer/Theme/ThemeUtil.ts @@ -0,0 +1,31 @@ +import { BrandVariants, Theme, createLightTheme } from "@fluentui/react-components"; +import { Platform } from "ConfigContext"; +import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme"; + +// These are the theme colors for Fluent UI 9 React components +const appThemePortalBrandRamp: BrandVariants = { + 10: "#020305", + 20: "#111723", + 30: "#16263D", + 40: "#193253", + 50: "#1B3F6A", + 60: "#1B4C82", + 70: "#18599B", + 80: "#1267B4", + 90: "#3174C2", + 100: "#4F82C8", + 110: "#6790CF", + 120: "#7D9ED5", + 130: "#92ACDC", + 140: "#A6BAE2", + 150: "#BAC9E9", + 160: "#CDD8EF", +}; + +export function getPlatformTheme(platform: Platform): Theme { + if (platform === Platform.Fabric) { + return createLightTheme(appThemeFabricTealBrandRamp); + } else { + return createLightTheme(appThemePortalBrandRamp); + } +} diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index d7c673620..fffacb134 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -1,5 +1,6 @@ import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { useNotebook } from "Explorer/Notebook/useNotebook"; +import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import * as ko from "knockout"; import * as _ from "underscore"; import * as Constants from "../../Common/Constants"; @@ -27,9 +28,7 @@ import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import ConflictsTab from "../Tabs/ConflictsTab"; -import DocumentsTab from "../Tabs/DocumentsTab"; import GraphTab from "../Tabs/GraphTab"; -import MongoDocumentsTab from "../Tabs/MongoDocumentsTab"; import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab"; import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; @@ -292,13 +291,13 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree, }); - const documentsTabs: DocumentsTab[] = useTabs + const documentsTabs: DocumentsTabV2[] = useTabs .getState() .getTabs( ViewModels.CollectionTabKind.Documents, (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(), - ) as DocumentsTab[]; - let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; + ) as DocumentsTabV2[]; + let documentsTab: DocumentsTabV2 = documentsTabs && documentsTabs[0]; if (documentsTab) { useTabs.getState().activateTab(documentsTab); @@ -312,7 +311,7 @@ export default class Collection implements ViewModels.Collection { }); this.documentIds([]); - documentsTab = new DocumentsTab({ + documentsTab = new DocumentsTabV2({ partitionKey: this.partitionKey, documentIds: ko.observableArray([]), tabKind: ViewModels.CollectionTabKind.Documents, @@ -494,13 +493,13 @@ export default class Collection implements ViewModels.Collection { dataExplorerArea: Constants.Areas.ResourceTree, }); - const mongoDocumentsTabs: MongoDocumentsTab[] = useTabs + const mongoDocumentsTabs: DocumentsTabV2[] = useTabs .getState() .getTabs( ViewModels.CollectionTabKind.Documents, (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(), - ) as MongoDocumentsTab[]; - let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0]; + ) as DocumentsTabV2[]; + let mongoDocumentsTab: DocumentsTabV2 = mongoDocumentsTabs && mongoDocumentsTabs[0]; if (mongoDocumentsTab) { useTabs.getState().activateTab(mongoDocumentsTab); @@ -514,7 +513,7 @@ export default class Collection implements ViewModels.Collection { }); this.documentIds([]); - mongoDocumentsTab = new MongoDocumentsTab({ + mongoDocumentsTab = new DocumentsTabV2({ partitionKey: this.partitionKey, documentIds: this.documentIds, tabKind: ViewModels.CollectionTabKind.Documents, diff --git a/src/Explorer/Tree/DocumentId.ts b/src/Explorer/Tree/DocumentId.ts index 4ccf71e40..c54931c75 100644 --- a/src/Explorer/Tree/DocumentId.ts +++ b/src/Explorer/Tree/DocumentId.ts @@ -1,10 +1,18 @@ import * as ko from "knockout"; import * as DataModels from "../../Contracts/DataModels"; import { useDialog } from "../Controls/Dialog"; -import DocumentsTab from "../Tabs/DocumentsTab"; +/** + * Replaces DocumentsTab so we can plug any object + */ +export interface IDocumentIdContainer { + partitionKeyProperties?: string[]; + partitionKey: DataModels.PartitionKey; + isEditorDirty: () => boolean; + selectDocument: (documentId: DocumentId) => Promise; +} export default class DocumentId { - public container: DocumentsTab; + public container: IDocumentIdContainer; public rid: string; public self: string; public ts: string; @@ -15,7 +23,7 @@ export default class DocumentId { public stringPartitionKeyValues: string[]; public isDirty: ko.Observable; - constructor(container: DocumentsTab, data: any, partitionKeyValue: any[]) { + constructor(container: IDocumentIdContainer, data: any, partitionKeyValue: any[]) { this.container = container; this.self = data._self; this.rid = data._rid; diff --git a/src/Explorer/Tree/ObjectId.ts b/src/Explorer/Tree/ObjectId.ts index fc53a6a37..314a4cd7e 100644 --- a/src/Explorer/Tree/ObjectId.ts +++ b/src/Explorer/Tree/ObjectId.ts @@ -1,9 +1,8 @@ import * as ko from "knockout"; -import DocumentId from "./DocumentId"; -import DocumentsTab from "../Tabs/DocumentsTab"; +import DocumentId, { IDocumentIdContainer } from "./DocumentId"; export default class ObjectId extends DocumentId { - constructor(container: DocumentsTab, data: any, partitionKeyValue: any) { + constructor(container: IDocumentIdContainer, data: any, partitionKeyValue: any) { super(container, data, partitionKeyValue); if (typeof data._id === "object") { this.id = ko.observable(data._id[Object.keys(data._id)[0]]); diff --git a/src/Explorer/Tree/ResourceTokenCollection.ts b/src/Explorer/Tree/ResourceTokenCollection.ts index a5e6ab07f..6212c14a8 100644 --- a/src/Explorer/Tree/ResourceTokenCollection.ts +++ b/src/Explorer/Tree/ResourceTokenCollection.ts @@ -1,3 +1,4 @@ +import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; @@ -7,7 +8,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { useTabs } from "../../hooks/useTabs"; import Explorer from "../Explorer"; -import DocumentsTab from "../Tabs/DocumentsTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import TabsBase from "../Tabs/TabsBase"; import { useDatabases } from "../useDatabases"; @@ -118,15 +118,15 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas dataExplorerArea: Constants.Areas.ResourceTree, }); - const documentsTabs: DocumentsTab[] = useTabs + const documentsTabs: DocumentsTabV2[] = useTabs .getState() .getTabs( ViewModels.CollectionTabKind.Documents, (tab: TabsBase) => tab.collection?.id() === this.id() && (tab.collection as ViewModels.CollectionBase).databaseId === this.databaseId, - ) as DocumentsTab[]; - let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0]; + ) as DocumentsTabV2[]; + let documentsTab: DocumentsTabV2 = documentsTabs && documentsTabs[0]; if (documentsTab) { useTabs.getState().activateTab(documentsTab); @@ -139,7 +139,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas tabTitle: "Items", }); - documentsTab = new DocumentsTab({ + documentsTab = new DocumentsTabV2({ partitionKey: this.partitionKey, resourceTokenPartitionKey: userContext.parsedResourceToken?.partitionKey, documentIds: ko.observableArray([]), diff --git a/src/Explorer/Tree2/ResourceTree.tsx b/src/Explorer/Tree2/ResourceTree.tsx index 761cda2c6..45e3ba5d4 100644 --- a/src/Explorer/Tree2/ResourceTree.tsx +++ b/src/Explorer/Tree2/ResourceTree.tsx @@ -1,14 +1,13 @@ import { - BrandVariants, FluentProvider, - Theme, Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent, - createLightTheme, } from "@fluentui/react-components"; +import { configContext } from "ConfigContext"; import { TreeNode2, TreeNode2Component } from "Explorer/Controls/TreeComponent2/TreeNode2Component"; +import { getPlatformTheme } from "Explorer/Theme/ThemeUtil"; import { useDatabaseTreeNodes } from "Explorer/Tree2/useDatabaseTreeNodes"; import * as React from "react"; import shallow from "zustand/shallow"; @@ -22,29 +21,6 @@ interface ResourceTreeProps { container: Explorer; } -const cosmosdb: BrandVariants = { - 10: "#020305", - 20: "#111723", - 30: "#16263D", - 40: "#193253", - 50: "#1B3F6A", - 60: "#1B4C82", - 70: "#18599B", - 80: "#1267B4", - 90: "#3174C2", - 100: "#4F82C8", - 110: "#6790CF", - 120: "#7D9ED5", - 130: "#92ACDC", - 140: "#A6BAE2", - 150: "#BAC9E9", - 160: "#CDD8EF", -}; - -const lightTheme: Theme = { - ...createLightTheme(cosmosdb), -}; - export const DATA_TREE_LABEL = "DATA"; /** @@ -113,7 +89,7 @@ export const ResourceTree2: React.FC = ({ container }: Resour return ( <> - + global).TextEncoder = TextEncoder; (global).TextDecoder = TextDecoder; + +(global).ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); From f669a99228bbdd00a92895064a35464f02c8a94a Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Wed, 29 May 2024 16:03:51 +0200 Subject: [PATCH 43/67] Separate Fabric-specific message types (#1848) * Update message de->fabric to v3 * Reinstate get authorization token path which doesn't get called every 5 minutes anymore * Remove obsolete comment * Add missing types * Fix format * Fix build issue * Revert "Reinstate get authorization token path which doesn't get called every 5 minutes anymore" This reverts commit a3f3511043201c8824989df04fd2ad813b88bd9e. * Keep 3 old fabric message types enums for compatibility with the portal * Re-add warning comment about not changing existing message type enums --------- Co-authored-by: Laurent Nguyen --- src/Common/CosmosClient.ts | 4 +-- src/Common/MessageHandler.ts | 3 ++- src/Contracts/DataExplorerMessagesContract.ts | 27 +++++-------------- src/Contracts/FabricMessageTypes.ts | 13 +++++++++ src/Contracts/FabricMessagesContract.ts | 2 +- src/Contracts/MessageTypes.ts | 14 ++++------ src/Platform/Fabric/FabricUtil.ts | 4 +-- src/hooks/useKnockoutExplorer.ts | 3 ++- 8 files changed, 33 insertions(+), 37 deletions(-) create mode 100644 src/Contracts/FabricMessageTypes.ts diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index f75c47454..c24680f4b 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -1,6 +1,6 @@ import * as Cosmos from "@azure/cosmos"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; -import { AuthorizationToken } from "Contracts/MessageTypes"; +import { AuthorizationToken } from "Contracts/FabricMessageTypes"; import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { AuthType } from "../AuthType"; @@ -59,7 +59,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { /* ************** TODO: Uncomment this code if we need to support these operations ************** // User master tokens const authorizationToken = await sendCachedDataMessage( - MessageTypes.GetAuthorizationToken, + FabricMessageTypes.GetAuthorizationToken, [requestInfo], userContext.fabricContext.connectionId, ); diff --git a/src/Common/MessageHandler.ts b/src/Common/MessageHandler.ts index 0d8304d95..378c58b5f 100644 --- a/src/Common/MessageHandler.ts +++ b/src/Common/MessageHandler.ts @@ -1,3 +1,4 @@ +import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import Q from "q"; import * as _ from "underscore"; import { MessageTypes } from "../Contracts/ExplorerContracts"; @@ -36,7 +37,7 @@ export function handleCachedDataMessage(message: any): void { * @returns */ export function sendCachedDataMessage( - messageType: MessageTypes, + messageType: MessageTypes | FabricMessageTypes, params: Object[], scope?: string, timeoutInMs?: number, diff --git a/src/Contracts/DataExplorerMessagesContract.ts b/src/Contracts/DataExplorerMessagesContract.ts index e47e4c384..a38940120 100644 --- a/src/Contracts/DataExplorerMessagesContract.ts +++ b/src/Contracts/DataExplorerMessagesContract.ts @@ -1,37 +1,22 @@ -import { MessageTypes } from "./MessageTypes"; +import { FabricMessageTypes } from "./FabricMessageTypes"; // This is the current version of these messages -export const DATA_EXPLORER_RPC_VERSION = "2"; +export const DATA_EXPLORER_RPC_VERSION = "3"; // Data Explorer to Fabric - -// TODO Remove when upgrading to Fabric v2 -export type DataExploreMessageV1 = - | "ready" +export type DataExploreMessageV3 = | { - type: MessageTypes.GetAuthorizationToken; - id: string; - params: GetCosmosTokenMessageOptions[]; - } - | { - type: MessageTypes.GetAllResourceTokens; - id: string; - }; -// ----------------------------- - -export type DataExploreMessageV2 = - | { - type: MessageTypes.Ready; + type: FabricMessageTypes.Ready; id: string; params: [string]; // version } | { - type: MessageTypes.GetAuthorizationToken; + type: FabricMessageTypes.GetAuthorizationToken; id: string; params: GetCosmosTokenMessageOptions[]; } | { - type: MessageTypes.GetAllResourceTokens; + type: FabricMessageTypes.GetAllResourceTokens; id: string; }; diff --git a/src/Contracts/FabricMessageTypes.ts b/src/Contracts/FabricMessageTypes.ts new file mode 100644 index 000000000..aa374472d --- /dev/null +++ b/src/Contracts/FabricMessageTypes.ts @@ -0,0 +1,13 @@ +/** + * Data Explorer -> Fabric communication. + */ +export enum FabricMessageTypes { + GetAuthorizationToken = "GetAuthorizationToken", + GetAllResourceTokens = "GetAllResourceTokens", + Ready = "Ready", +} + +export interface AuthorizationToken { + XDate: string; + PrimaryReadWriteToken: string; +} diff --git a/src/Contracts/FabricMessagesContract.ts b/src/Contracts/FabricMessagesContract.ts index b381fd845..dcf5a5a50 100644 --- a/src/Contracts/FabricMessagesContract.ts +++ b/src/Contracts/FabricMessagesContract.ts @@ -1,4 +1,4 @@ -import { AuthorizationToken } from "./MessageTypes"; +import { AuthorizationToken } from "Contracts/FabricMessageTypes"; // This is the version of these messages export const FABRIC_RPC_VERSION = "2"; diff --git a/src/Contracts/MessageTypes.ts b/src/Contracts/MessageTypes.ts index a19b69e5f..4550d2a53 100644 --- a/src/Contracts/MessageTypes.ts +++ b/src/Contracts/MessageTypes.ts @@ -1,12 +1,13 @@ /** * Messaging types used with Data Explorer <-> Portal communication, - * Hosted <-> Explorer communication and Data Explorer -> Fabric communication. + * Hosted <-> Explorer communication * * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! * WARNING: !!!!!!! YOU CAN ONLY ADD NEW TYPES TO THE END OF THIS ENUM !!!!!!! * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! * * Enum are integers, so inserting or deleting a type will break the communication. + * */ export enum MessageTypes { TelemetryInfo, @@ -43,14 +44,9 @@ export enum MessageTypes { DisplayNPSSurvey, OpenVCoreMongoNetworkingBlade, OpenVCoreMongoConnectionStringsBlade, - GetAuthorizationToken, // Data Explorer -> Fabric - GetAllResourceTokens, // Data Explorer -> Fabric - Ready, // Data Explorer -> Fabric + GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums. + GetAllResourceTokens, // unused. Can be removed if the portal uses the same list of enums. + Ready, // unused. Can be removed if the portal uses the same list of enums. OpenCESCVAFeedbackBlade, ActivateTab, } - -export interface AuthorizationToken { - XDate: string; - PrimaryReadWriteToken: string; -} diff --git a/src/Platform/Fabric/FabricUtil.ts b/src/Platform/Fabric/FabricUtil.ts index 6c6d2ef9c..26ba859ff 100644 --- a/src/Platform/Fabric/FabricUtil.ts +++ b/src/Platform/Fabric/FabricUtil.ts @@ -1,6 +1,6 @@ import { sendCachedDataMessage } from "Common/MessageHandler"; +import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; -import { MessageTypes } from "Contracts/MessageTypes"; import { updateUserContext, userContext } from "UserContext"; import { logConsoleError } from "Utils/NotificationConsoleUtils"; @@ -19,7 +19,7 @@ const requestDatabaseResourceTokens = async (): Promise => { lastRequestTimestamp = Date.now(); try { const fabricDatabaseConnectionInfo = await sendCachedDataMessage( - MessageTypes.GetAllResourceTokens, + FabricMessageTypes.GetAllResourceTokens, [], userContext.fabricContext.connectionId, ); diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 4e80b3476..3a6061acd 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -1,5 +1,6 @@ import { createUri } from "Common/UrlUtility"; import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract"; +import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract"; import Explorer from "Explorer/Explorer"; import { useSelectedNode } from "Explorer/useSelectedNode"; @@ -156,7 +157,7 @@ async function configureFabric(): Promise { ); sendMessage({ - type: MessageTypes.Ready, + type: FabricMessageTypes.Ready, id: "ready", params: [DATA_EXPLORER_RPC_VERSION], }); From cebf044803e58a56710adbecaf1bcf05587b8627 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Wed, 29 May 2024 18:20:07 +0200 Subject: [PATCH 44/67] Fix typo (#1849) --- src/Contracts/ViewModels.ts | 12 ++--- src/Explorer/Tabs/ConflictsTab.ts | 46 +++++++++--------- .../DocumentsTabV2/DocumentsTabV2.test.tsx | 30 ++++++------ .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 48 +++++++++---------- src/Explorer/Tabs/ScriptTabBase.ts | 12 ++--- .../StoredProcedureTabComponent.tsx | 2 +- src/Explorer/Tabs/TriggerTabContent.tsx | 2 +- .../Tabs/UserDefinedFunctionTabContent.tsx | 2 +- 8 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 66fd54ec4..e50ba6769 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -324,9 +324,9 @@ export enum DocumentExplorerState { noDocumentSelected, newDocumentValid, newDocumentInvalid, - exisitingDocumentNoEdits, - exisitingDocumentDirtyValid, - exisitingDocumentDirtyInvalid, + existingDocumentNoEdits, + existingDocumentDirtyValid, + existingDocumentDirtyInvalid, } export enum IndexingPolicyEditorState { @@ -339,9 +339,9 @@ export enum IndexingPolicyEditorState { export enum ScriptEditorState { newInvalid, newValid, - exisitingNoEdits, - exisitingDirtyValid, - exisitingDirtyInvalid, + existingNoEdits, + existingDirtyValid, + existingDirtyInvalid, } export enum CollectionTabKind { diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index c01c206c1..15355bd63 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -6,16 +6,16 @@ import DiscardIcon from "../../../images/discard.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; import * as Constants from "../../Common/Constants"; import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants"; -import { createDocument } from "../../Common/dataAccess/createDocument"; -import { deleteConflict } from "../../Common/dataAccess/deleteConflict"; -import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; -import { queryConflicts } from "../../Common/dataAccess/queryConflicts"; -import { updateDocument } from "../../Common/dataAccess/updateDocument"; import editable from "../../Common/EditableUtility"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import * as HeadersUtility from "../../Common/HeadersUtility"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; +import { createDocument } from "../../Common/dataAccess/createDocument"; +import { deleteConflict } from "../../Common/dataAccess/deleteConflict"; +import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; +import { queryConflicts } from "../../Common/dataAccess/queryConflicts"; +import { updateDocument } from "../../Common/dataAccess/updateDocument"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; @@ -117,15 +117,15 @@ export default class ConflictsTab extends TabsBase { this.isEditorDirty = ko.computed(() => { switch (this.editorState()) { case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.existingDocumentNoEdits: return false; case ViewModels.DocumentExplorerState.newDocumentValid: case ViewModels.DocumentExplorerState.newDocumentInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid: return true; - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: return ( this.selectedConflictCurrent.getEditableOriginalValue() !== this.selectedConflictCurrent.getEditableCurrentValue() @@ -139,8 +139,8 @@ export default class ConflictsTab extends TabsBase { this.acceptChangesButton = { enabled: ko.computed(() => { switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentNoEdits: return true; } @@ -155,8 +155,8 @@ export default class ConflictsTab extends TabsBase { this.discardButton = { enabled: ko.computed(() => { switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid: return true; } @@ -171,8 +171,8 @@ export default class ConflictsTab extends TabsBase { this.deleteButton = { enabled: ko.computed(() => { switch (this.editorState()) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentNoEdits: return true; } @@ -247,7 +247,7 @@ export default class ConflictsTab extends TabsBase { return Q(); } - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits); return Q(); } @@ -407,22 +407,22 @@ export default class ConflictsTab extends TabsBase { public onDiscardClick = (): Q.Promise => { this.selectedConflictContent(this.selectedConflictContent.getEditableOriginalValue()); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits); return Q(); }; public onValidDocumentEdit(): Q.Promise { - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); + this.editorState(ViewModels.DocumentExplorerState.existingDocumentDirtyValid); return Q(); } public onInvalidDocumentEdit(): Q.Promise { if ( - this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || - this.editorState() === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid + this.editorState() === ViewModels.DocumentExplorerState.existingDocumentNoEdits || + this.editorState() === ViewModels.DocumentExplorerState.existingDocumentDirtyValid ) { - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); + this.editorState(ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid); return Q(); } @@ -555,7 +555,7 @@ export default class ConflictsTab extends TabsBase { let parsedConflictContent: any = JSON.parse(documentToInsert); const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4); this.selectedConflictContent.setBaseline(renderedConflictContent); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits); } return Q(); @@ -576,7 +576,7 @@ export default class ConflictsTab extends TabsBase { const renderedConflictContent: string = this.renderObjectForEditor(parsedConflictContent, null, 4); this.selectedConflictContent.setBaseline(renderedConflictContent); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits); } return Q(); @@ -588,7 +588,7 @@ export default class ConflictsTab extends TabsBase { parsedDocumentToDelete = ConflictsTab.removeSystemProperties(parsedDocumentToDelete); const renderedDocumentToDelete: string = this.renderObjectForEditor(parsedDocumentToDelete, null, 4); this.selectedConflictContent.setBaseline(renderedDocumentToDelete); - this.editorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + this.editorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits); } return Q(); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx index d224c2f9a..3d2b1e663 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx @@ -179,17 +179,17 @@ describe("Documents tab (noSql API)", () => { testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: true, visible: true }); testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: true }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, + state: ViewModels.DocumentExplorerState.existingDocumentNoEdits, enabled: false, visible: false, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid, enabled: false, visible: false, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid, enabled: false, visible: false, }); @@ -211,17 +211,17 @@ describe("Documents tab (noSql API)", () => { testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: true, visible: true }); testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: true, visible: true }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, + state: ViewModels.DocumentExplorerState.existingDocumentNoEdits, enabled: false, visible: false, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid, enabled: false, visible: false, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid, enabled: false, visible: false, }); @@ -243,17 +243,17 @@ describe("Documents tab (noSql API)", () => { testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false }); testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, + state: ViewModels.DocumentExplorerState.existingDocumentNoEdits, enabled: false, visible: true, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid, enabled: true, visible: true, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid, enabled: false, visible: true, }); @@ -275,17 +275,17 @@ describe("Documents tab (noSql API)", () => { testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false }); testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, + state: ViewModels.DocumentExplorerState.existingDocumentNoEdits, enabled: false, visible: true, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid, enabled: true, visible: true, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid, enabled: true, visible: true, }); @@ -306,14 +306,14 @@ describe("Documents tab (noSql API)", () => { testCases.add({ state: ViewModels.DocumentExplorerState.noDocumentSelected, enabled: false, visible: false }); testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentValid, enabled: false, visible: false }); testCases.add({ state: ViewModels.DocumentExplorerState.newDocumentInvalid, enabled: false, visible: false }); - testCases.add({ state: ViewModels.DocumentExplorerState.exisitingDocumentNoEdits, enabled: true, visible: true }); + testCases.add({ state: ViewModels.DocumentExplorerState.existingDocumentNoEdits, enabled: true, visible: true }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyValid, enabled: true, visible: true, }); testCases.add({ - state: ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid, + state: ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid, enabled: true, visible: true, }); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 1fb6ed9db..06d472d5b 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -146,7 +146,7 @@ export const getDiscardNewDocumentChangesButtonState = (editorState: ViewModels. export const getSaveExistingDocumentButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ enabled: (() => { switch (editorState) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: return true; default: return false; @@ -155,9 +155,9 @@ export const getSaveExistingDocumentButtonState = (editorState: ViewModels.Docum visible: (() => { switch (editorState) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentNoEdits: + case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: return true; default: return false; @@ -169,8 +169,8 @@ export const getSaveExistingDocumentButtonState = (editorState: ViewModels.Docum export const getDiscardExistingDocumentChangesButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ enabled: (() => { switch (editorState) { - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: return true; default: return false; @@ -179,9 +179,9 @@ export const getDiscardExistingDocumentChangesButtonState = (editorState: ViewMo visible: (() => { switch (editorState) { - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentNoEdits: + case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: return true; default: return false; @@ -368,7 +368,7 @@ const getNewDocumentButtonState = (editorState: ViewModels.DocumentExplorerState enabled: (() => { switch (editorState) { case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.existingDocumentNoEdits: return true; default: return false; @@ -588,15 +588,15 @@ export const DocumentsTabComponent: React.FunctionComponent { switch (editorState) { case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + case ViewModels.DocumentExplorerState.existingDocumentNoEdits: return false; case ViewModels.DocumentExplorerState.newDocumentValid: case ViewModels.DocumentExplorerState.newDocumentInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid: return true; - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + case ViewModels.DocumentExplorerState.existingDocumentDirtyValid: return true; default: @@ -685,7 +685,7 @@ export const DocumentsTabComponent: React.FunctionComponent { @@ -1234,10 +1234,10 @@ export const DocumentsTabComponent: React.FunctionComponent(); @@ -77,15 +77,15 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode this.editorState(ViewModels.ScriptEditorState.newInvalid); } break; - case ViewModels.ScriptEditorState.exisitingDirtyInvalid: - case ViewModels.ScriptEditorState.exisitingDirtyValid: + case ViewModels.ScriptEditorState.existingDirtyInvalid: + case ViewModels.ScriptEditorState.existingDirtyValid: if (isValid) { - this.editorState(ViewModels.ScriptEditorState.exisitingDirtyValid); + this.editorState(ViewModels.ScriptEditorState.existingDirtyValid); } else { - this.editorState(ViewModels.ScriptEditorState.exisitingDirtyInvalid); + this.editorState(ViewModels.ScriptEditorState.existingDirtyInvalid); } break; - case ViewModels.ScriptEditorState.exisitingDirtyValid: + case ViewModels.ScriptEditorState.existingDirtyValid: default: break; } diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index ce8c6ac45..1f230b305 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -404,7 +404,7 @@ export default class StoredProcedureTabComponent extends React.Component< this.node = this.collection.createStoredProcedureNode(createdResource); this.props.scriptTabBaseInstance.node = this.node; useTabs.getState().updateTab(this.props.scriptTabBaseInstance); - this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); + this.props.scriptTabBaseInstance.editorState(ViewModels.ScriptEditorState.existingNoEdits); this.setState({ executeButton: { diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index 5fd28502a..28e81ee38 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -114,7 +114,7 @@ export class TriggerTabContent extends Component Date: Wed, 29 May 2024 09:56:27 -0700 Subject: [PATCH 45/67] Use new Fluent-based Resource Tree for all environments (#1841) Co-authored-by: Laurent Nguyen --- .eslintignore | 2 +- src/Common/ResourceTreeContainer.tsx | 12 +- src/Contracts/ViewModels.ts | 5 + src/Explorer/ContextMenuButtonFactory.tsx | 2 +- ....test.tsx => LegacyTreeComponent.test.tsx} | 54 +- ...eComponent.tsx => LegacyTreeComponent.tsx} | 95 +- .../TreeComponent/TreeNodeComponent.test.tsx | 180 ++ .../TreeComponent/TreeNodeComponent.tsx | 206 ++ ...snap => LegacyTreeComponent.test.tsx.snap} | 34 +- .../TreeNodeComponent.test.tsx.snap | 1927 +++++++++++++++ .../TreeComponent2/TreeNode2Component.tsx | 147 -- src/Explorer/Explorer.tsx | 8 - src/Explorer/Panes/AddCollectionPanel.tsx | 3 +- .../Panes/PanelContainerComponent.tsx | 1 + .../PanelContainerComponent.test.tsx.snap | 2 + src/Explorer/Tree/ResourceTokenTree.tsx | 65 - src/Explorer/Tree/ResourceTree.tsx | 776 ++---- .../Tree/ResourceTreeAdapter.test.tsx | 12 +- src/Explorer/Tree/ResourceTreeAdapter.tsx | 147 +- src/Explorer/Tree/SampleDataTree.tsx | 8 +- .../ResourceTreeAdapter.test.tsx.snap | 2 +- .../__snapshots__/treeNodeUtil.test.ts.snap | 2074 +++++++++++++++++ src/Explorer/Tree/treeNodeUtil.test.ts | 602 +++++ .../treeNodeUtil.ts} | 233 +- src/Explorer/Tree2/ResourceTree.tsx | 107 - src/Explorer/Tree2/useDatabaseTreeNodes.ts | 90 - src/UserContext.ts | 2 +- src/Utils/PromiseSource.ts | 18 + test/cassandra/container.spec.ts | 38 +- test/graph/container.spec.ts | 34 +- test/mongo/container.spec.ts | 45 +- test/mongo/container32.spec.ts | 41 +- test/sql/container.spec.ts | 38 +- test/sql/resourceToken.spec.ts | 16 +- test/tables/container.spec.ts | 27 +- test/testExplorer/TestExplorer.ts | 4 +- test/utils/shared.ts | 49 + utils/generateResourceToken.js | 93 +- 38 files changed, 5866 insertions(+), 1333 deletions(-) rename src/Explorer/Controls/TreeComponent/{TreeComponent.test.tsx => LegacyTreeComponent.test.tsx} (70%) rename src/Explorer/Controls/TreeComponent/{TreeComponent.tsx => LegacyTreeComponent.tsx} (77%) create mode 100644 src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx create mode 100644 src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx rename src/Explorer/Controls/TreeComponent/__snapshots__/{TreeComponent.test.tsx.snap => LegacyTreeComponent.test.tsx.snap} (94%) create mode 100644 src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap delete mode 100644 src/Explorer/Controls/TreeComponent2/TreeNode2Component.tsx delete mode 100644 src/Explorer/Tree/ResourceTokenTree.tsx create mode 100644 src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap create mode 100644 src/Explorer/Tree/treeNodeUtil.test.ts rename src/Explorer/{Tree2/containerTreeNodeUtil.ts => Tree/treeNodeUtil.ts} (59%) delete mode 100644 src/Explorer/Tree2/ResourceTree.tsx delete mode 100644 src/Explorer/Tree2/useDatabaseTreeNodes.ts create mode 100644 src/Utils/PromiseSource.ts diff --git a/.eslintignore b/.eslintignore index 20cdeb4c3..f7ed8b5f9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -125,7 +125,7 @@ src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx -src/Explorer/Controls/TreeComponent/TreeComponent.tsx +src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx diff --git a/src/Common/ResourceTreeContainer.tsx b/src/Common/ResourceTreeContainer.tsx index e3968c112..0259c42e2 100644 --- a/src/Common/ResourceTreeContainer.tsx +++ b/src/Common/ResourceTreeContainer.tsx @@ -1,14 +1,10 @@ -import { ResourceTree } from "Explorer/Tree/ResourceTree"; import React, { FunctionComponent, MutableRefObject, useEffect, useRef } 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 { ResourceTree2 } from "../Explorer/Tree2/ResourceTree"; +import { ResourceTree } from "../Explorer/Tree/ResourceTree"; import { userContext } from "../UserContext"; import { getApiShortDisplayName } from "../Utils/APITypeUtils"; -import { Platform, configContext } from "./../ConfigContext"; import { NormalizedEventKey } from "./Constants"; export interface ResourceTreeContainerProps { @@ -74,12 +70,8 @@ export const ResourceTreeContainer: FunctionComponent
- {userContext.authType === AuthType.ResourceToken ? ( - - ) : userContext.features.enableKoResourceTree ? ( + {userContext.features.enableKoResourceTree ? (
- ) : configContext.platform === Platform.Fabric ? ( - ) : ( )} diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index e50ba6769..af69452f1 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -176,6 +176,11 @@ export interface Collection extends CollectionBase { loadTriggers(): Promise; loadOffer(): Promise; + showStoredProcedures: ko.Observable; + showTriggers: ko.Observable; + showUserDefinedFunctions: ko.Observable; + showConflicts: ko.Observable; + createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure; createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction; createTriggerNode(data: TriggerDefinition | SqlTriggerResource): Trigger; diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index d9f9f3439..90c78e365 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -1,3 +1,4 @@ +import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { useDatabases } from "Explorer/useDatabases"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; @@ -19,7 +20,6 @@ import { userContext } from "../UserContext"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { useSidePanel } from "../hooks/useSidePanel"; import { Platform, configContext } from "./../ConfigContext"; -import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent"; import Explorer from "./Explorer"; import { useNotebook } from "./Notebook/useNotebook"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx b/src/Explorer/Controls/TreeComponent/LegacyTreeComponent.test.tsx similarity index 70% rename from src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx rename to src/Explorer/Controls/TreeComponent/LegacyTreeComponent.test.tsx index 1138f0b45..fdb0b8fee 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.test.tsx +++ b/src/Explorer/Controls/TreeComponent/LegacyTreeComponent.test.tsx @@ -1,48 +1,48 @@ -import React from "react"; import { shallow } from "enzyme"; -import { TreeComponent, TreeNode, TreeNodeComponent } from "./TreeComponent"; +import React from "react"; +import { LegacyTreeComponent, LegacyTreeNode, LegacyTreeNodeComponent } from "./LegacyTreeComponent"; -const buildChildren = (): TreeNode[] => { - const grandChild11: TreeNode = { +const buildChildren = (): LegacyTreeNode[] => { + const grandChild11: LegacyTreeNode = { label: "ZgrandChild11", }; - const grandChild12: TreeNode = { + const grandChild12: LegacyTreeNode = { label: "AgrandChild12", }; - const child1: TreeNode = { + const child1: LegacyTreeNode = { label: "Bchild1", children: [grandChild11, grandChild12], }; - const child2: TreeNode = { + const child2: LegacyTreeNode = { label: "2child2", }; return [child1, child2]; }; -const buildChildren2 = (): TreeNode[] => { - const grandChild11: TreeNode = { +const buildChildren2 = (): LegacyTreeNode[] => { + const grandChild11: LegacyTreeNode = { label: "ZgrandChild11", }; - const grandChild12: TreeNode = { + const grandChild12: LegacyTreeNode = { label: "AgrandChild12", }; - const child1: TreeNode = { + const child1: LegacyTreeNode = { label: "aChild", }; - const child2: TreeNode = { + const child2: LegacyTreeNode = { label: "bchild", children: [grandChild11, grandChild12], }; - const child3: TreeNode = { + const child3: LegacyTreeNode = { label: "cchild", }; - const child4: TreeNode = { + const child4: LegacyTreeNode = { label: "dchild", children: [grandChild11, grandChild12], }; @@ -50,7 +50,7 @@ const buildChildren2 = (): TreeNode[] => { return [child1, child2, child3, child4]; }; -describe("TreeComponent", () => { +describe("LegacyTreeComponent", () => { it("renders a simple tree", () => { const root = { label: "root", @@ -62,14 +62,14 @@ describe("TreeComponent", () => { className: "tree", }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); -describe("TreeNodeComponent", () => { +describe("LegacyTreeNodeComponent", () => { it("renders a simple node (sorted children, expanded)", () => { - const node: TreeNode = { + const node: LegacyTreeNode = { label: "label", id: "id", children: buildChildren(), @@ -98,12 +98,12 @@ describe("TreeNodeComponent", () => { generation: 12, paddingLeft: 23, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it("renders unsorted children by default", () => { - const node: TreeNode = { + const node: LegacyTreeNode = { label: "label", children: buildChildren(), isExpanded: true, @@ -113,12 +113,12 @@ describe("TreeNodeComponent", () => { generation: 2, paddingLeft: 9, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it("does not render children by default", () => { - const node: TreeNode = { + const node: LegacyTreeNode = { label: "label", children: buildChildren(), isAlphaSorted: false, @@ -128,12 +128,12 @@ describe("TreeNodeComponent", () => { generation: 2, paddingLeft: 9, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it("renders sorted children, expanded, leaves and parents separated", () => { - const node: TreeNode = { + const node: LegacyTreeNode = { label: "label", id: "id", children: buildChildren2(), @@ -156,12 +156,12 @@ describe("TreeNodeComponent", () => { generation: 12, paddingLeft: 23, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it("renders loading icon", () => { - const node: TreeNode = { + const node: LegacyTreeNode = { label: "label", children: [], isExpanded: true, @@ -172,7 +172,7 @@ describe("TreeNodeComponent", () => { generation: 2, paddingLeft: 9, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx b/src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx similarity index 77% rename from src/Explorer/Controls/TreeComponent/TreeComponent.tsx rename to src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx index 4cf601047..80a682dda 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx @@ -12,6 +12,7 @@ import { IContextualMenuItemProps, IContextualMenuProps, } from "@fluentui/react"; +import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import * as React from "react"; import AnimateHeight from "react-animate-height"; import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif"; @@ -22,18 +23,10 @@ import { StyleConstants } from "../../../Common/StyleConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -export interface TreeNodeMenuItem { - label: string; - onClick: () => void; - iconSrc?: string; - isDisabled?: boolean; - styleClass?: string; -} - -export interface TreeNode { +export interface LegacyTreeNode { label: string; id?: string; - children?: TreeNode[]; + children?: LegacyTreeNode[]; contextMenu?: TreeNodeMenuItem[]; iconSrc?: string; isExpanded?: boolean; @@ -50,34 +43,37 @@ export interface TreeNode { onContextMenuOpen?: () => void; } -export interface TreeComponentProps { - rootNode: TreeNode; +export interface LegacyTreeComponentProps { + rootNode: LegacyTreeNode; style?: any; className?: string; } -export class TreeComponent extends React.Component { +export class LegacyTreeComponent extends React.Component { public render(): JSX.Element { return (
- +
); } } /* Tree node is a react component */ -interface TreeNodeComponentProps { - node: TreeNode; +interface LegacyTreeNodeComponentProps { + node: LegacyTreeNode; generation: number; paddingLeft: number; } -interface TreeNodeComponentState { +interface LegacyTreeNodeComponentState { isExpanded: boolean; isMenuShowing: boolean; } -export class TreeNodeComponent extends React.Component { +export class LegacyTreeNodeComponent extends React.Component< + LegacyTreeNodeComponentProps, + LegacyTreeNodeComponentState +> { private static readonly paddingPerGenerationPx = 16; private static readonly iconOffset = 22; private static readonly transitionDurationMS = 200; @@ -85,7 +81,7 @@ export class TreeNodeComponent extends React.Component(); private isExpanded: boolean; - constructor(props: TreeNodeComponentProps) { + constructor(props: LegacyTreeNodeComponentProps) { super(props); this.isExpanded = props.node.isExpanded; this.state = { @@ -94,13 +90,13 @@ export class TreeNodeComponent extends React.Component a.label.localeCompare(b.label); + const compareFct = (a: LegacyTreeNode, b: LegacyTreeNode) => a.label.localeCompare(b.label); let unsortedChildren; if (treeNode.isLeavesParentsSeparate) { // Separate parents and leave - const parents: TreeNode[] = treeNode.children.filter((node) => node.children); - const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children); + const parents: LegacyTreeNode[] = treeNode.children.filter((node) => node.children); + const leaves: LegacyTreeNode[] = treeNode.children.filter((node) => !node.children); if (treeNode.isAlphaSorted) { parents.sort(compareFct); @@ -141,18 +137,18 @@ export class TreeNodeComponent extends React.Component !!child.children); + const childrenWithSubChildren = node.children.filter((child: LegacyTreeNode) => !!child.children); if (childrenWithSubChildren.length > 0) { - additionalOffsetPx = TreeNodeComponent.iconOffset; + additionalOffsetPx = LegacyTreeNodeComponent.iconOffset; } } @@ -160,10 +156,10 @@ export class TreeNodeComponent extends React.Component
{node.children && ( - +
- {TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => ( - ( + - previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child), + (previous: boolean, child: LegacyTreeNode) => + previous || + (child.isSelected && child.isSelected()) || + LegacyTreeNodeComponent.isAnyDescendantSelected(child), false, ) ); @@ -232,10 +233,10 @@ export class TreeNodeComponent extends React.Component { - this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent.createClickEvent()); + this.contextMenuRef.current.firstChild.dispatchEvent(LegacyTreeNodeComponent.createClickEvent()); }; - private renderContextMenuButton(node: TreeNode): JSX.Element { + private renderContextMenuButton(node: LegacyTreeNode): JSX.Element { const menuItemLabel = "More"; const buttonStyles: Partial = { rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` }, @@ -265,7 +266,7 @@ export class TreeNodeComponent extends React.Component e.target.dispatchEvent(TreeNodeComponent.createClickEvent())} + onContextMenu={(e) => e.target.dispatchEvent(LegacyTreeNodeComponent.createClickEvent())} > {props.item.onRenderIcon()} ; } @@ -314,12 +315,12 @@ export class TreeNodeComponent extends React.Component, node: TreeNode): void => { + private onNodeClick = (event: React.MouseEvent, node: LegacyTreeNode): void => { event.stopPropagation(); if (node.children) { const isExpanded = !this.state.isExpanded; // Prevent collapsing if node header is blank - if (!(TreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) { + if (!(LegacyTreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) { this.setState({ isExpanded }); } } @@ -327,14 +328,14 @@ export class TreeNodeComponent extends React.Component, node: TreeNode): void => { + private onNodeKeyPress = (event: React.KeyboardEvent, node: LegacyTreeNode): void => { if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { event.stopPropagation(); this.props.node.onClick && this.props.node.onClick(this.state.isExpanded); } }; - private onCollapseExpandIconKeyPress = (event: React.KeyboardEvent, node: TreeNode): void => { + private onCollapseExpandIconKeyPress = (event: React.KeyboardEvent, node: LegacyTreeNode): void => { if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { event.stopPropagation(); if (node.children) { diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx new file mode 100644 index 000000000..602edcb2a --- /dev/null +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx @@ -0,0 +1,180 @@ +import { TreeItem, TreeItemLayout, tokens } from "@fluentui/react-components"; +import PromiseSource from "Utils/PromiseSource"; +import { mount, shallow } from "enzyme"; +import React from "react"; +import { act } from "react-dom/test-utils"; +import { TreeNode, TreeNodeComponent } from "./TreeNodeComponent"; + +function generateTestNode(id: string, additionalProps?: Partial): TreeNode { + const node: TreeNode = { + id, + label: `${id}Label`, + className: `${id}Class`, + iconSrc: `${id}Icon`, + onClick: jest.fn().mockName(`${id}Click`), + ...additionalProps, + }; + return node; +} + +describe("TreeNodeComponent", () => { + it("renders a single node", () => { + const node = generateTestNode("root"); + const component = shallow(); + expect(component).toMatchSnapshot(); + + // The "click" handler is actually attached to onOpenChange, with a type of "Click". + component + .find(TreeItem) + .props() + .onOpenChange(null!, { open: true, value: "borp", target: null!, event: null!, type: "Click" }); + expect(node.onClick).toHaveBeenCalled(); + }); + + it("renders a node with a menu", () => { + const node = generateTestNode("root", { + contextMenu: [ + { + label: "enabledItem", + onClick: jest.fn().mockName("enabledItemClick"), + }, + { + label: "disabledItem", + onClick: jest.fn().mockName("disabledItemClick"), + isDisabled: true, + }, + ], + }); + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it("renders a loading spinner if the node is loading", async () => { + const loading = new PromiseSource(); + const node = generateTestNode("root", { + onExpanded: () => loading.promise, + }); + const component = shallow(); + + act(() => { + component + .find(TreeItem) + .props() + .onOpenChange(null!, { open: true, value: "borp", target: null!, event: null!, type: "ExpandIconClick" }); + }); + + expect(component).toMatchSnapshot("loading"); + await loading.resolveAndWait(); + expect(component).toMatchSnapshot("loaded"); + }); + + it("renders single selected leaf node as selected", () => { + const node = generateTestNode("root", { + isSelected: () => true, + }); + const component = shallow(); + expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual( + tokens.colorNeutralBackground1Selected, + ); + expect(component).toMatchSnapshot(); + }); + + it("renders selected parent node as selected if no descendant nodes are selected", () => { + const node = generateTestNode("root", { + isSelected: () => true, + children: [ + generateTestNode("child1", { + children: [generateTestNode("grandchild1"), generateTestNode("grandchild2")], + }), + generateTestNode("child2"), + ], + }); + const component = shallow(); + expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual( + tokens.colorNeutralBackground1Selected, + ); + expect(component).toMatchSnapshot(); + }); + + it("renders selected parent node as unselected if any descendant node is selected", () => { + const node = generateTestNode("root", { + isSelected: () => true, + children: [ + generateTestNode("child1", { + children: [ + generateTestNode("grandchild1", { + isSelected: () => true, + }), + generateTestNode("grandchild2"), + ], + }), + generateTestNode("child2"), + ], + }); + const component = shallow(); + expect(component.find(TreeItemLayout).props().style?.backgroundColor).toBeUndefined(); + expect(component).toMatchSnapshot(); + }); + + it("renders an icon if the node has one", () => { + const node = generateTestNode("root", { + iconSrc: "the-icon.svg", + }); + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it("does not render children if the node is loading", () => { + const node = generateTestNode("root", { + isLoading: true, + children: [ + generateTestNode("child1", { + children: [generateTestNode("grandchild1"), generateTestNode("grandchild2")], + }), + generateTestNode("child2"), + ], + }); + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it("fully renders a tree", () => { + const child3Loading = new PromiseSource(); + const node = generateTestNode("root", { + isSelected: () => true, + children: [ + generateTestNode("child1", { + children: [ + generateTestNode("grandchild1", { + iconSrc: "grandchild1Icon.svg", + isSelected: () => true, + }), + generateTestNode("grandchild2"), + ], + }), + generateTestNode("child2Loading", { + isLoading: true, + children: [generateTestNode("grandchild3NotRendered")], + }), + generateTestNode("child3Expanding", { + onExpanded: () => child3Loading.promise, + }), + ], + }); + const component = mount(); + + // Find and expand the child3Expanding node + const expandingChild = component.find(TreeItem).filterWhere((n) => n.props().value === "root/child3ExpandingLabel"); + act(() => { + expandingChild.props().onOpenChange(null!, { + open: true, + value: "root/child3ExpandingLabel", + target: null!, + event: null!, + type: "Click", + }); + }); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx new file mode 100644 index 000000000..1717530ec --- /dev/null +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx @@ -0,0 +1,206 @@ +import { + Button, + Menu, + MenuItem, + MenuList, + MenuOpenChangeData, + MenuOpenEvent, + MenuPopover, + MenuTrigger, + Spinner, + Tree, + TreeItem, + TreeItemLayout, + TreeOpenChangeData, + TreeOpenChangeEvent, +} from "@fluentui/react-components"; +import { MoreHorizontal20Regular } from "@fluentui/react-icons"; +import { tokens } from "@fluentui/react-theme"; +import * as React from "react"; +import { useCallback } from "react"; + +export interface TreeNodeMenuItem { + label: string; + onClick: () => void; + iconSrc?: string; + isDisabled?: boolean; + styleClass?: string; +} + +export interface TreeNode { + label: string; + id?: string; + children?: TreeNode[]; + contextMenu?: TreeNodeMenuItem[]; + iconSrc?: string; + isExpanded?: boolean; + className?: string; + isAlphaSorted?: boolean; + // data?: any; // Piece of data corresponding to this node + timestamp?: number; + isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves + isLoading?: boolean; + isScrollable?: boolean; + isSelected?: () => boolean; + onClick?: () => void; // Only if a leaf, other click will expand/collapse + onExpanded?: () => Promise; + onCollapsed?: () => void; + onContextMenuOpen?: () => void; +} + +export interface TreeNodeComponentProps { + node: TreeNode; + className?: string; + treeNodeId: string; +} + +/** Function that returns true if any descendant (at any depth) of this node is selected. */ +function isAnyDescendantSelected(node: TreeNode): boolean { + return ( + node.children && + node.children.reduce( + (previous: boolean, child: TreeNode) => + previous || (child.isSelected && child.isSelected()) || isAnyDescendantSelected(child), + false, + ) + ); +} + +const getTreeIcon = (iconSrc: string): JSX.Element => ; + +export const TreeNodeComponent: React.FC = ({ + node, + treeNodeId, +}: TreeNodeComponentProps): JSX.Element => { + const [isLoading, setIsLoading] = React.useState(false); + + const getSortedChildren = (treeNode: TreeNode): TreeNode[] => { + if (!treeNode || !treeNode.children) { + return undefined; + } + + const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label); + + let unsortedChildren; + if (treeNode.isLeavesParentsSeparate) { + // Separate parents and leave + const parents: TreeNode[] = treeNode.children.filter((node) => node.children); + const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children); + + if (treeNode.isAlphaSorted) { + parents.sort(compareFct); + leaves.sort(compareFct); + } + + unsortedChildren = parents.concat(leaves); + } else { + unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children; + } + + return unsortedChildren; + }; + + const isBranch = node.children?.length > 0; + + const onOpenChange = useCallback( + (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => { + if (data.type === "Click" && !isBranch && node.onClick) { + node.onClick(); + } + if (!node.isExpanded && data.open && node.onExpanded) { + // Catch the transition non-expanded to expanded + setIsLoading(true); + node.onExpanded?.().then(() => setIsLoading(false)); + } else if (node.isExpanded && !data.open && node.onCollapsed) { + // Catch the transition expanded to non-expanded + node.onCollapsed?.(); + } + }, + [isBranch, node, setIsLoading], + ); + + const onMenuOpenChange = useCallback( + (e: MenuOpenEvent, data: MenuOpenChangeData) => { + if (data.open) { + node.onContextMenuOpen?.(); + } + }, + [node], + ); + + // We show a node as selected if it is selected AND no descendant is selected. + // We want to show only the deepest selected node as selected. + const isCurrentNodeSelected = node.isSelected && node.isSelected(); + const shouldShowAsSelected = isCurrentNodeSelected && !isAnyDescendantSelected(node); + + const contextMenuItems = (node.contextMenu ?? []).map((menuItem) => ( + + {menuItem.label} + + )); + + const treeItem = ( + + 0 && ( + + + + ) + } + expandIcon={isLoading ? : undefined} + iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)} + style={{ + backgroundColor: shouldShowAsSelected ? tokens.colorNeutralBackground1Selected : undefined, + }} + > + {node.label} + + {!node.isLoading && node.children?.length > 0 && ( + + {getSortedChildren(node).map((childNode: TreeNode) => ( + + ))} + + )} + + ); + + if (contextMenuItems.length === 0) { + return treeItem; + } + + // For accessibility, it's highly recommended that any 'actions' also be available in the context menu. + // See https://react.fluentui.dev/?path=/docs/components-tree--default#actions + return ( + + {treeItem} + + {contextMenuItems} + + + ); +}; diff --git a/src/Explorer/Controls/TreeComponent/__snapshots__/TreeComponent.test.tsx.snap b/src/Explorer/Controls/TreeComponent/__snapshots__/LegacyTreeComponent.test.tsx.snap similarity index 94% rename from src/Explorer/Controls/TreeComponent/__snapshots__/TreeComponent.test.tsx.snap rename to src/Explorer/Controls/TreeComponent/__snapshots__/LegacyTreeComponent.test.tsx.snap index dda586200..afa484490 100644 --- a/src/Explorer/Controls/TreeComponent/__snapshots__/TreeComponent.test.tsx.snap +++ b/src/Explorer/Controls/TreeComponent/__snapshots__/LegacyTreeComponent.test.tsx.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TreeComponent renders a simple tree 1`] = ` +exports[`LegacyTreeComponent renders a simple tree 1`] = `
- `; -exports[`TreeNodeComponent does not render children by default 1`] = ` +exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
- - `; -exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`] = ` +exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
- - `; -exports[`TreeNodeComponent renders loading icon 1`] = ` +exports[`LegacyTreeNodeComponent renders loading icon 1`] = `
`; -exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = ` +exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
- - - - `; -exports[`TreeNodeComponent renders unsorted children by default 1`] = ` +exports[`LegacyTreeNodeComponent renders unsorted children by default 1`] = `
- - + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > + rootLabel + + +`; + +exports[`TreeNodeComponent fully renders a tree 1`] = ` + + +
+
, + }, + "isActionsVisible": false, + "isAsideVisible": false, + "itemType": "branch", + "layoutRef": Object { + "current":
+ + +
+ rootLabel +
+
, + }, + "open": false, + "selectionRef": Object { + "current": null, + }, + "subtreeRef": Object { + "current": null, + }, + "treeItemRef": Object { + "current": , + }, + "value": "root", + } + } + > + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > +
+
+ + + + + + + +
+
+ +
+
+ rootLabel +
+
+
+ + + +
+ + +
+
, + }, + "isActionsVisible": false, + "isAsideVisible": false, + "itemType": "branch", + "layoutRef": Object { + "current":
+ + +
+ child1Label +
+
, + }, + "open": false, + "selectionRef": Object { + "current": null, + }, + "subtreeRef": Object { + "current": null, + }, + "treeItemRef": Object { + "current": , + }, + "value": "root/child1Label", + } + } + > + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > +
+
+ + + + + + + +
+
+ +
+
+ child1Label +
+
+
+ + + + +
+
+
+ + +
+
, + }, + "isActionsVisible": false, + "isAsideVisible": false, + "itemType": "branch", + "layoutRef": Object { + "current":
+ + +
+ child2LoadingLabel +
+
, + }, + "open": false, + "selectionRef": Object { + "current": null, + }, + "subtreeRef": Object { + "current": null, + }, + "treeItemRef": Object { + "current": , + }, + "value": "root/child2LoadingLabel", + } + } + > + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > +
+
+ + + + + + + +
+
+ +
+
+ child2LoadingLabel +
+
+
+ +
+ +
+ + +
+
, + }, + "isActionsVisible": false, + "isAsideVisible": false, + "itemType": "leaf", + "layoutRef": Object { + "current":
+ + +
+ child3ExpandingLabel +
+
, + }, + "open": false, + "selectionRef": Object { + "current": null, + }, + "subtreeRef": Object { + "current": null, + }, + "treeItemRef": Object { + "current":
+
+ + +
+ child3ExpandingLabel +
+
+
, + }, + "value": "root/child3ExpandingLabel", + } + } + > + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > +
+
+ +
+
+ child3ExpandingLabel +
+
+
+ +
+ + +
+ + + + +
+ +
+`; + +exports[`TreeNodeComponent renders a loading spinner if the node is loading: loaded 1`] = ` + + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > + rootLabel + + +`; + +exports[`TreeNodeComponent renders a loading spinner if the node is loading: loading 1`] = ` + + + } + iconBefore={ + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > + rootLabel + + +`; + +exports[`TreeNodeComponent renders a node with a menu 1`] = ` + + + + + + + } + className="rootClass" + data-test="TreeNode:root" + iconBefore={ + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > + rootLabel + + + + + + + enabledItem + + + disabledItem + + + + +`; + +exports[`TreeNodeComponent renders a single node 1`] = ` + + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > + rootLabel + + +`; + +exports[`TreeNodeComponent renders an icon if the node has one 1`] = ` + + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > + rootLabel + + +`; + +exports[`TreeNodeComponent renders selected parent node as selected if no descendant nodes are selected 1`] = ` + + + } + style={ + Object { + "backgroundColor": "var(--colorNeutralBackground1Selected)", + } + } + > + rootLabel + + + + + + +`; + +exports[`TreeNodeComponent renders selected parent node as unselected if any descendant node is selected 1`] = ` + + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > + rootLabel + + + + + + +`; + +exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = ` + + + } + style={ + Object { + "backgroundColor": "var(--colorNeutralBackground1Selected)", + } + } + > + rootLabel + + +`; diff --git a/src/Explorer/Controls/TreeComponent2/TreeNode2Component.tsx b/src/Explorer/Controls/TreeComponent2/TreeNode2Component.tsx deleted file mode 100644 index 14c66db12..000000000 --- a/src/Explorer/Controls/TreeComponent2/TreeNode2Component.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { - Button, - Menu, - MenuItem, - MenuList, - MenuPopover, - MenuTrigger, - Spinner, - Tree, - TreeItem, - TreeItemLayout, - TreeOpenChangeData, - TreeOpenChangeEvent, -} from "@fluentui/react-components"; -import { MoreHorizontal20Regular } from "@fluentui/react-icons"; -import { tokens } from "@fluentui/react-theme"; -import * as React from "react"; - -export interface TreeNode2MenuItem { - label: string; - onClick: () => void; - iconSrc?: string; - isDisabled?: boolean; - styleClass?: string; -} - -export interface TreeNode2 { - label: string; - id?: string; - children?: TreeNode2[]; - contextMenu?: TreeNode2MenuItem[]; - iconSrc?: string; - isExpanded?: boolean; - className?: string; - isAlphaSorted?: boolean; - // data?: any; // Piece of data corresponding to this node - timestamp?: number; - isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves - isLoading?: boolean; - isScrollable?: boolean; - isSelected?: () => boolean; - onClick?: () => void; // Only if a leaf, other click will expand/collapse - onExpanded?: () => Promise; - onCollapsed?: () => void; - onContextMenuOpen?: () => void; -} - -export interface TreeNode2ComponentProps { - node: TreeNode2; - className?: string; - treeNodeId: string; -} - -const getTreeIcon = (iconSrc: string): JSX.Element => ; - -export const TreeNode2Component: React.FC = ({ - node, - treeNodeId, -}: TreeNode2ComponentProps): JSX.Element => { - const [isLoading, setIsLoading] = React.useState(false); - - const getSortedChildren = (treeNode: TreeNode2): TreeNode2[] => { - if (!treeNode || !treeNode.children) { - return undefined; - } - - const compareFct = (a: TreeNode2, b: TreeNode2) => a.label.localeCompare(b.label); - - let unsortedChildren; - if (treeNode.isLeavesParentsSeparate) { - // Separate parents and leave - const parents: TreeNode2[] = treeNode.children.filter((node) => node.children); - const leaves: TreeNode2[] = treeNode.children.filter((node) => !node.children); - - if (treeNode.isAlphaSorted) { - parents.sort(compareFct); - leaves.sort(compareFct); - } - - unsortedChildren = parents.concat(leaves); - } else { - unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children; - } - - return unsortedChildren; - }; - - const onOpenChange = (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => { - if (!node.isExpanded && data.open && node.onExpanded) { - // Catch the transition non-expanded to expanded - setIsLoading(true); - node.onExpanded?.().then(() => setIsLoading(false)); - } else if (node.isExpanded && !data.open && node.onCollapsed) { - // Catch the transition expanded to non-expanded - node.onCollapsed?.(); - } - }; - - return ( - - - -
+
+
+
+ + Default Query Results View + + + Select the default view to use when displaying query results. + +
+
+ +
+
+
diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 532ce4662..bfcaa9bda 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; +import { SplitterDirection } from "Common/Splitter"; import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; @@ -12,7 +13,13 @@ import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { KeyboardAction } from "KeyboardShortcuts"; import { QueryConstants } from "Shared/Constants"; -import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; +import { + LocalStorageUtility, + StorageKey, + getDefaultQueryResultsView, + getRUThreshold, + ruThresholdEnabled, +} from "Shared/StorageUtility"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { TabsState, useTabs } from "hooks/useTabs"; @@ -25,6 +32,7 @@ import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; import DownloadQueryIcon from "../../../../images/DownloadQuery.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; +import CheckIcon from "../../../../images/check-1.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg"; import { NormalizedEventKey } from "../../../Common/Constants"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; @@ -103,6 +111,7 @@ interface IQueryTabStates { cancelQueryTimeoutID: NodeJS.Timeout; copilotActive: boolean; currentTabActive: boolean; + queryResultsView: SplitterDirection; } export const QueryTabFunctionComponent = (props: IQueryTabComponentProps): any => { @@ -147,6 +156,7 @@ export default class QueryTabComponent extends React.Component this._setViewLayout(SplitterDirection.Vertical), + hasPopup: false, + }; + const horizontalButton: CommandButtonComponentProps = { + isSelected: this.state.queryResultsView === SplitterDirection.Horizontal, + iconSrc: this.state.queryResultsView === SplitterDirection.Horizontal ? CheckIcon : undefined, + commandButtonLabel: "Horizontal", + ariaLabel: "Horizontal", + onCommandClick: () => this._setViewLayout(SplitterDirection.Horizontal), + hasPopup: false, + }; + + return { + commandButtonLabel: "View", + ariaLabel: "View", + hasPopup: true, + children: [verticalButton, horizontalButton], + }; + } + private _setViewLayout(direction: SplitterDirection): void { + this.setState({ queryResultsView: direction }); + + // We'll need to refresh the context buttons to update the selected state of the view buttons + setTimeout(() => { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + }, 100); + } + private _toggleCopilot = (active: boolean) => { this.setState({ copilotActive: active }); useQueryCopilot.getState().setCopilotEnabledforExecution(active); @@ -634,7 +680,12 @@ export default class QueryTabComponent extends React.Component )}
- +
{ @@ -51,4 +53,12 @@ export const getRUThreshold = (): number => { return DefaultRUThreshold; }; +export const getDefaultQueryResultsView = (): SplitterDirection => { + const defaultQueryResultsViewRaw = LocalStorageUtility.getEntryString(StorageKey.DefaultQueryResultsView); + if (defaultQueryResultsViewRaw === SplitterDirection.Horizontal) { + return SplitterDirection.Horizontal; + } + return SplitterDirection.Vertical; +}; + export const DefaultRUThreshold = 5000; From 736731474f9da69839766b63989d323bcdac5946 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 5 Jun 2024 12:27:29 -0700 Subject: [PATCH 55/67] show an expand icon for nodes with non-null children arrray (#1862) --- .../TreeComponent/TreeNodeComponent.test.tsx | 14 +++++++ .../TreeComponent/TreeNodeComponent.tsx | 3 +- .../TreeNodeComponent.test.tsx.snap | 38 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx index 602edcb2a..e0cdf5700 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx @@ -124,6 +124,20 @@ describe("TreeNodeComponent", () => { expect(component).toMatchSnapshot(); }); + it("renders a node as expandable if it has empty, but defined, children array", () => { + const node = generateTestNode("root", { + isLoading: true, + children: [ + generateTestNode("child1", { + children: [], + }), + generateTestNode("child2"), + ], + }); + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + it("does not render children if the node is loading", () => { const node = generateTestNode("root", { isLoading: true, diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx index 1717530ec..354046222 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx @@ -100,7 +100,8 @@ export const TreeNodeComponent: React.FC = ({ return unsortedChildren; }; - const isBranch = node.children?.length > 0; + // A branch node is any node with a defined children array, even if the array is empty. + const isBranch = !!node.children; const onOpenChange = useCallback( (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => { diff --git a/src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap b/src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap index 6678f29ad..a20da1f66 100644 --- a/src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap +++ b/src/Explorer/Controls/TreeComponent/__snapshots__/TreeNodeComponent.test.tsx.snap @@ -1534,6 +1534,44 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa `; +exports[`TreeNodeComponent renders a node as expandable if it has empty, but defined, children array 1`] = ` + + + } + style={ + Object { + "backgroundColor": undefined, + } + } + > + rootLabel + + +`; + exports[`TreeNodeComponent renders a node with a menu 1`] = ` Date: Wed, 5 Jun 2024 12:46:32 -0700 Subject: [PATCH 56/67] Update Playwright, improve E2E test reliability, add scripts to deploy test resources (#1857) --- .editorconfig | 10 + .eslintignore | 2 + .github/workflows/ci.yml | 140 +- .gitignore | 6 +- jest-playwright.config.js | 13 - jest.config.playwright.js | 7 - package-lock.json | 1426 +---------------- package.json | 4 +- playwright.config.ts | 53 + .../TreeComponent/LegacyTreeComponent.tsx | 5 +- .../TreeComponent/TreeNodeComponent.tsx | 1 + .../LegacyTreeComponent.test.tsx.snap | 15 +- .../TreeNodeComponent.test.tsx.snap | 24 + .../Menus/CommandBar/CommandBarUtil.tsx | 2 +- ...teCollectionConfirmationPane.test.tsx.snap | 6 + .../Panes/DeleteDatabaseConfirmationPanel.tsx | 16 +- .../ExecuteSprocParamsPane.test.tsx.snap | 6 + src/Explorer/Panes/PanelFooterComponent.tsx | 1 + .../__snapshots__/RightPaneForm.test.tsx.snap | 6 + .../StringInputPane.test.tsx.snap | 6 + .../TableQuerySelectPanel.test.tsx.snap | 6 + .../AddTableEntityPanel.test.tsx.snap | 6 + .../EditTableEntityPanel.test.tsx.snap | 6 + ...eteDatabaseConfirmationPanel.test.tsx.snap | 29 +- src/HostedExplorer.tsx | 1 + src/Main.tsx | 2 +- .../Hosted/Components/ConnectExplorer.tsx | 2 +- src/SelfServe/SelfServeComponent.tsx | 2 +- .../SelfServeComponent.test.tsx.snap | 4 + test/README.md | 143 ++ test/cassandra/container.spec.ts | 78 +- test/fx.ts | 151 ++ test/graph/container.spec.ts | 58 - test/gremlin/container.spec.ts | 41 + test/mongo/container.spec.ts | 101 +- test/mongo/container32.spec.ts | 59 - test/notebooks/GettingStarted.ipynb | 110 -- test/notebooks/upload.spec.ts | 25 - test/playwrightEnv.js | 26 - test/resources/README.md | 4 + test/resources/account.bicep | 50 + test/resources/all-accounts.bicep | 31 + test/resources/create-resource-group.ps1 | 31 + test/resources/deploy.ps1 | 113 ++ test/scripts/check-test-accounts.ps1 | 22 + test/scripts/set-test-accounts.ps1 | 69 + test/selfServe/selfServeExample.spec.ts | 41 - test/sql/container.spec.ts | 77 +- test/sql/resourceToken.spec.ts | 51 +- test/sql/selfServeExample.spec.ts | 23 + test/tables/container.spec.ts | 50 +- test/testExplorer/TestExplorer.ts | 10 + test/utils.js | 21 - test/utils/shared.ts | 69 - test/utils/waitForExplorer.ts | 9 - webpack.config.js | 23 + 56 files changed, 1176 insertions(+), 2117 deletions(-) create mode 100644 .editorconfig delete mode 100644 jest-playwright.config.js delete mode 100644 jest.config.playwright.js create mode 100644 playwright.config.ts create mode 100644 test/README.md create mode 100644 test/fx.ts delete mode 100644 test/graph/container.spec.ts create mode 100644 test/gremlin/container.spec.ts delete mode 100644 test/mongo/container32.spec.ts delete mode 100644 test/notebooks/GettingStarted.ipynb delete mode 100644 test/notebooks/upload.spec.ts delete mode 100644 test/playwrightEnv.js create mode 100644 test/resources/README.md create mode 100644 test/resources/account.bicep create mode 100644 test/resources/all-accounts.bicep create mode 100644 test/resources/create-resource-group.ps1 create mode 100644 test/resources/deploy.ps1 create mode 100644 test/scripts/check-test-accounts.ps1 create mode 100644 test/scripts/set-test-accounts.ps1 delete mode 100644 test/selfServe/selfServeExample.spec.ts create mode 100644 test/sql/selfServeExample.spec.ts delete mode 100644 test/utils.js delete mode 100644 test/utils/shared.ts delete mode 100644 test/utils/waitForExplorer.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..65096ce0b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# NOTE: Prettier reads EditorConfig settings, so be careful adjusting settings here and assuming they'll only affect your editor ;). + +# top-most EditorConfig file +root = true + +[*.yml] +indent_size = 2 + +[*.{js,jsx,ts,tsx}] +indent_size = 2 \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index f7ed8b5f9..2eeecaed5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,5 @@ +playwright.config.ts + **/node_modules/ src/**/__mocks__/**/* dist/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd097a194..15c555e31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,79 +104,6 @@ jobs: run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true env: PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} - endtoendemulator: - name: "End To End Emulator Tests" - # Temporarily disabled. This test needs to be rewritten in playwright - if: false - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: 18.x - - uses: southpolesteve/cosmos-emulator-github-action@v1 - - name: End to End Tests - run: | - npm ci - npm start & - npm run wait-for-server - npx jest -c ./jest.config.e2e.js --detectOpenHandles test/sql/container.spec.ts - shell: bash - env: - DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator" - PLATFORM: "Emulator" - NODE_TLS_REJECT_UNAUTHORIZED: 0 - - uses: actions/upload-artifact@v3 - if: failure() - with: - name: screenshots - path: failed-* - endtoend: - name: "E2E" - runs-on: ubuntu-latest - env: - NODE_TLS_REJECT_UNAUTHORIZED: 0 - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - strategy: - fail-fast: false - matrix: - test-file: - - ./test/cassandra/container.spec.ts - - ./test/graph/container.spec.ts - - ./test/sql/container.spec.ts - - ./test/mongo/container.spec.ts - - ./test/mongo/container32.spec.ts - - ./test/selfServe/selfServeExample.spec.ts - - ./test/sql/resourceToken.spec.ts - - ./test/tables/container.spec.ts - steps: - - uses: actions/checkout@v4 - - - name: "Az CLI login" - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Use Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: 18.x - - run: npm ci - - run: npm start & - - run: npm run wait-for-server - - name: ${{ matrix['test-file'] }} - run: | - # Run tests up to three times - for i in $(seq 1 3); do npx jest -c ./jest.config.playwright.js ${{ matrix['test-file'] }} && s=0 && break || s=$? && sleep 1; done; (exit $s) - shell: bash - - uses: actions/upload-artifact@v3 - if: failure() - with: - name: screenshots - path: screenshots/ nuget: name: Publish Nuget if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') @@ -226,3 +153,70 @@ jobs: name: packages with: path: "*.nupkg" + + playwright-tests: + name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})" + runs-on: ubuntu-latest + env: + NODE_TLS_REJECT_UNAUTHORIZED: 0 + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] + steps: + - uses: actions/checkout@v4 + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + - run: npm ci + - run: npx playwright install --with-deps + - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} + run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: blob-report-${{ matrix.shardIndex }} + path: blob-report + retention-days: 1 + + merge-playwright-reports: + name: "Merge Playwright Reports" + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() }} + needs: [playwright-tests] + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + path: all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./all-blob-reports + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 14 \ No newline at end of file diff --git a/.gitignore b/.gitignore index be016240b..3617cf905 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,8 @@ Contracts/* .env failure.png screenshots/* -GettingStarted-ignore*.ipynb \ No newline at end of file +GettingStarted-ignore*.ipynb +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/jest-playwright.config.js b/jest-playwright.config.js deleted file mode 100644 index d66e8f221..000000000 --- a/jest-playwright.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const isCI = require("is-ci"); - -module.exports = { - exitOnPageError: false, - launchOptions: { - headless: isCI, - slowMo: 10, - timeout: 60000, - }, - contextOptions: { - ignoreHTTPSErrors: true, - }, -}; diff --git a/jest.config.playwright.js b/jest.config.playwright.js deleted file mode 100644 index c452a5368..000000000 --- a/jest.config.playwright.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: "jest-playwright-preset", - testMatch: ["/test/**/*.spec.[jt]s?(x)"], - setupFiles: ["dotenv/config"], - testEnvironment: "./test/playwrightEnv.js", - setupFilesAfterEnv: ["expect-playwright"], -}; diff --git a/package-lock.json b/package-lock.json index 31e3d37db..5a872d3fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,6 +122,7 @@ "@babel/preset-env": "7.9.0", "@babel/preset-react": "7.9.4", "@babel/preset-typescript": "7.9.0", + "@playwright/test": "1.44.0", "@testing-library/react": "11.2.3", "@types/applicationinsights-js": "1.0.7", "@types/codemirror": "0.0.56", @@ -166,7 +167,6 @@ "eslint-plugin-no-null": "1.0.2", "eslint-plugin-prefer-arrow": "1.2.3", "eslint-plugin-react-hooks": "4.6.0", - "expect-playwright": "0.3.3", "fast-glob": "3.2.5", "fs-extra": "7.0.0", "html-inline-css-webpack-plugin": "1.11.2", @@ -175,7 +175,6 @@ "html-webpack-plugin": "5.5.3", "jest": "26.6.3", "jest-canvas-mock": "2.3.1", - "jest-playwright-preset": "1.5.1", "jest-react-hooks-shallow": "1.5.1", "jest-trx-results-processor": "0.0.7", "less": "3.8.1", @@ -184,7 +183,6 @@ "mini-css-extract-plugin": "2.1.0", "monaco-editor-webpack-plugin": "7.1.0", "node-fetch": "2.6.7", - "playwright": "1.13.0", "prettier": "3.0.3", "process": "0.11.10", "querystring-es3": "0.2.1", @@ -10210,6 +10208,53 @@ "@phosphor/virtualdom": "^1.2.0" } }, + "node_modules/@playwright/test": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz", + "integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==", + "dev": true, + "dependencies": { + "playwright": "1.44.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@playwright/test/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz", + "integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==", + "dev": true, + "dependencies": { + "playwright-core": "1.44.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.23", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", @@ -11806,6 +11851,7 @@ "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", "dev": true, + "peer": true, "dependencies": { "@hapi/hoek": "^9.0.0" } @@ -11814,13 +11860,15 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -13580,15 +13628,6 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, - "node_modules/@types/wait-on": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.4.tgz", - "integrity": "sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -13611,16 +13650,6 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/youtube-player": { "version": "5.5.6", "resolved": "https://registry.npmjs.org/@types/youtube-player/-/youtube-player-5.5.6.tgz", @@ -14314,19 +14343,6 @@ "node": ">= 6.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/airbnb-prop-types": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", @@ -14553,18 +14569,6 @@ "dev": true, "peer": true }, - "node_modules/append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", - "dev": true, - "dependencies": { - "default-require-extensions": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/applicationinsights": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.8.0.tgz", @@ -14584,12 +14588,6 @@ "optional": true, "peer": true }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -15667,15 +15665,6 @@ "ieee754": "^1.1.4" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -15714,57 +15703,6 @@ "node": ">=0.10.0" } }, - "node_modules/caching-transform": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", - "dev": true, - "dependencies": { - "hasha": "^5.0.0", - "make-dir": "^3.0.0", - "package-hash": "^4.0.0", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/caching-transform/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caching-transform/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/caching-transform/node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -16210,15 +16148,6 @@ "node": ">=0.10.0" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/clean-webpack-plugin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", @@ -17285,19 +17214,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, - "node_modules/cwd": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", - "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==", - "dev": true, - "dependencies": { - "find-pkg": "^0.1.2", - "fs-exists-sync": "^0.1.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/d3": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", @@ -18083,12 +17999,6 @@ "node": ">=8" } }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true - }, "node_modules/deep-diff": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", @@ -18191,21 +18101,6 @@ "node": ">=10.17.0" } }, - "node_modules/default-require-extensions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", - "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", - "dev": true, - "dependencies": { - "strip-bom": "^4.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -19167,12 +19062,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, "node_modules/es6-templates": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", @@ -20079,15 +19968,6 @@ "node": ">= 10.14.2" } }, - "node_modules/expect-playwright": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/expect-playwright/-/expect-playwright-0.3.3.tgz", - "integrity": "sha512-uoeyx2D5LawJdziMdweOp6cnZzFOOPT9VvPG6gOh6YC7N9pU0k2KpVlRiz/Vc/fFBiGUNNeJq2Aq+9GJ65Nfrw==", - "dev": true, - "peerDependencies": { - "playwright-core": "^1.9.1" - } - }, "node_modules/expect/node_modules/@jest/types": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", @@ -20350,26 +20230,6 @@ "node": ">=0.10.0" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -20558,15 +20418,6 @@ "bser": "2.1.1" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -20685,124 +20536,6 @@ "node": ">=6" } }, - "node_modules/find-file-up": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", - "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==", - "dev": true, - "dependencies": { - "fs-exists-sync": "^0.1.0", - "resolve-dir": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-pkg": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", - "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==", - "dev": true, - "dependencies": { - "find-file-up": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-process": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.7.tgz", - "integrity": "sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "commander": "^5.1.0", - "debug": "^4.1.1" - }, - "bin": { - "find-process": "bin/find-process.js" - } - }, - "node_modules/find-process/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/find-process/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/find-process/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/find-process/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/find-process/node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/find-process/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-process/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -20968,19 +20701,6 @@ "node": ">=0.10.0" } }, - "node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -21284,41 +21004,12 @@ "node": ">= 0.6" } }, - "node_modules/fromentries": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", - "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "optional": true }, - "node_modules/fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fs-extra": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", @@ -21967,31 +21658,6 @@ "node": ">=0.10.0" } }, - "node_modules/hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "dependencies": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hasha/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/hasher": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hasher/-/hasher-1.2.0.tgz", @@ -22106,18 +21772,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -23701,18 +23355,6 @@ "node": ">=6" } }, - "node_modules/istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "dev": true, - "dependencies": { - "append-transform": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-instrument": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", @@ -23738,44 +23380,6 @@ "semver": "bin/semver.js" } }, - "node_modules/istanbul-lib-processinfo": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", - "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", - "dev": true, - "dependencies": { - "archy": "^1.0.0", - "cross-spawn": "^7.0.3", - "istanbul-lib-coverage": "^3.2.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -24070,217 +23674,6 @@ "node": ">=8" } }, - "node_modules/jest-circus": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-26.6.3.tgz", - "integrity": "sha512-ACrpWZGcQMpbv13XbzRzpytEJlilP/Su0JtNCi5r/xLpOUhnaIJr8leYYpLEMgPFURZISEHrnnpmB54Q/UziPw==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/babel__traverse": "^7.0.4", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^26.6.2", - "is-generator-fn": "^2.0.0", - "jest-each": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2", - "stack-utils": "^2.0.2", - "throat": "^5.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-circus/node_modules/@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-circus/node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/jest-circus/node_modules/@types/yargs": { - "version": "15.0.19", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", - "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-circus/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-circus/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jest-circus/node_modules/jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "dependencies": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-circus/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/jest-cli": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", @@ -26622,38 +26015,6 @@ "node": ">=8" } }, - "node_modules/jest-playwright-preset": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/jest-playwright-preset/-/jest-playwright-preset-1.5.1.tgz", - "integrity": "sha512-zsFAe61V72vSLkd1fCcf7YbHmbdAB82SLBdUuCUF43aODIojshQEDF88KdWL9P+4JQ+DvEABT+6sFX4sY0rR2w==", - "dev": true, - "dependencies": { - "expect-playwright": "^0.3.3", - "jest-circus": "^26.6.3", - "jest-environment-node": "^26.6.2", - "jest-process-manager": "^0.2.9", - "jest-runner": "^26.6.3", - "nyc": "^15.1.0", - "playwright-core": ">=1.2.0", - "rimraf": "^3.0.2", - "uuid": "^8.3.2" - } - }, - "node_modules/jest-playwright-preset/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -26670,113 +26031,6 @@ } } }, - "node_modules/jest-process-manager": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/jest-process-manager/-/jest-process-manager-0.2.9.tgz", - "integrity": "sha512-IKVdOSz1NLwKg9HTeyEDn63waMvKK6wcS+tCarGquNIktlXt4zAW2cfJ9vAA/xBcidWYKOPXHvy1l1N8qfw3Ww==", - "dev": true, - "dependencies": { - "@types/wait-on": "^5.2.0", - "chalk": "^4.1.0", - "cwd": "^0.10.0", - "exit": "^0.1.2", - "find-process": "^1.4.4", - "prompts": "^2.4.0", - "signal-exit": "^3.0.3", - "spawnd": "^4.4.0", - "tree-kill": "^1.2.2", - "wait-on": "^5.2.1" - } - }, - "node_modules/jest-process-manager/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-process-manager/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-process-manager/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-process-manager/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-process-manager/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-process-manager/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-process-manager/node_modules/wait-on": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-5.3.0.tgz", - "integrity": "sha512-DwrHrnTK+/0QFaB9a8Ol5Lna3k7WvUR4jzSKmz0YaPBpuN2sACyiPVKVfj6ejnjcajAcvn3wlbTyMIn9AZouOg==", - "dev": true, - "dependencies": { - "axios": "^0.21.1", - "joi": "^17.3.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "rxjs": "^6.6.3" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/jest-react-hooks-shallow": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz", @@ -28713,6 +27967,7 @@ "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", "dev": true, + "peer": true, "dependencies": { "@hapi/hoek": "^9.0.0", "@hapi/topo": "^5.0.0", @@ -28721,12 +27976,6 @@ "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/jpeg-js": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", - "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", - "dev": true - }, "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", @@ -32465,18 +31714,6 @@ "dev": true, "optional": true }, - "node_modules/node-preload": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", - "dev": true, - "dependencies": { - "process-on-spawn": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", @@ -32591,205 +31828,6 @@ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, - "node_modules/nyc": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", - "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", - "dev": true, - "dependencies": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^2.0.0", - "get-package-type": "^0.1.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "make-dir": "^3.0.0", - "node-preload": "^0.2.1", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", - "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "yargs": "^15.0.2" - }, - "bin": { - "nyc": "bin/nyc.js" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/nyc/node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/nyc/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nyc/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/nyc/node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -33198,15 +32236,6 @@ "node": ">=8" } }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/os-name": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", @@ -33308,21 +32337,6 @@ "node": ">=6" } }, - "node_modules/package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/param-case": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", @@ -33374,15 +32388,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -33718,12 +32723,6 @@ "node": ">=8" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -33803,39 +32802,10 @@ "node": ">=8" } }, - "node_modules/playwright": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.13.0.tgz", - "integrity": "sha512-GA5OyEeKx1v/pRcANmYncCT67Y7Y4N5zLRU5E690dn/Id10sooR5hQZmCDYsjXlutZb/1q0R3sITALnvhEjCjg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "commander": "^6.1.0", - "debug": "^4.1.1", - "extract-zip": "^2.0.1", - "https-proxy-agent": "^5.0.0", - "jpeg-js": "^0.4.2", - "mime": "^2.4.6", - "pngjs": "^5.0.0", - "progress": "^2.0.3", - "proper-lockfile": "^4.1.1", - "proxy-from-env": "^1.1.0", - "rimraf": "^3.0.2", - "stack-utils": "^2.0.3", - "ws": "^7.4.6", - "yazl": "^2.5.1" - }, - "bin": { - "playwright": "lib/cli/cli.js" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/playwright-core": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", - "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz", + "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -33844,42 +32814,6 @@ "node": ">=16" } }, - "node_modules/playwright/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/playwright/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/playwright/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/plotly.js-cartesian-dist-min": { "version": "1.52.3", "resolved": "https://registry.npmjs.org/plotly.js-cartesian-dist-min/-/plotly.js-cartesian-dist-min-1.52.3.tgz", @@ -33895,15 +32829,6 @@ "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/polygon-offset": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/polygon-offset/-/polygon-offset-0.3.1.tgz", @@ -34315,27 +33240,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/process-on-spawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", - "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", - "dev": true, - "dependencies": { - "fromentries": "^1.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -34384,26 +33288,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/property-information": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", @@ -34438,12 +33322,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -35806,18 +34684,6 @@ "node": ">= 0.10" } }, - "node_modules/release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", - "dev": true, - "dependencies": { - "es6-error": "^4.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/remark-parse": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz", @@ -36153,80 +35019,6 @@ "node": ">=8" } }, - "node_modules/resolve-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", - "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==", - "dev": true, - "dependencies": { - "expand-tilde": "^1.2.2", - "global-modules": "^0.2.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-dir/node_modules/expand-tilde": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", - "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==", - "dev": true, - "dependencies": { - "os-homedir": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-dir/node_modules/global-modules": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", - "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==", - "dev": true, - "dependencies": { - "global-prefix": "^0.1.4", - "is-windows": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-dir/node_modules/global-prefix": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", - "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.0", - "ini": "^1.3.4", - "is-windows": "^0.2.0", - "which": "^1.2.12" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-dir/node_modules/is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-dir/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -37397,59 +36189,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", - "dev": true, - "dependencies": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/spawn-wrap/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/spawn-wrap/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/spawnd": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-4.4.0.tgz", - "integrity": "sha512-jLPOfB6QOEgMOQY15Z6+lwZEhH3F5ncXxIaZ7WHPIapwNNLyjrs61okj3VJ3K6tmP5TZ6cO0VAu9rEY4MD4YQg==", - "dev": true, - "dependencies": { - "exit": "^0.1.2", - "signal-exit": "^3.0.2", - "tree-kill": "^1.2.2", - "wait-port": "^0.2.7" - } - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -38589,15 +37328,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", @@ -39789,29 +38519,6 @@ "node": ">=8.9.0" } }, - "node_modules/wait-port": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.14.tgz", - "integrity": "sha512-kIzjWcr6ykl7WFbZd0TMae8xovwqcqbx6FM9l+7agOgUByhzdjfzZBPK2CPufldTOMxbUivss//Sh9MFawmPRQ==", - "dev": true, - "dependencies": { - "chalk": "^2.4.2", - "commander": "^3.0.2", - "debug": "^4.1.1" - }, - "bin": { - "wait-port": "bin/wait-port.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wait-port/node_modules/commander": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", - "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", - "dev": true - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -40951,25 +39658,6 @@ "node": ">=8" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3" - } - }, "node_modules/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 d27273af9..d29fdebd6 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@babel/preset-env": "7.9.0", "@babel/preset-react": "7.9.4", "@babel/preset-typescript": "7.9.0", + "@playwright/test": "1.44.0", "@testing-library/react": "11.2.3", "@types/applicationinsights-js": "1.0.7", "@types/codemirror": "0.0.56", @@ -161,7 +162,6 @@ "eslint-plugin-no-null": "1.0.2", "eslint-plugin-prefer-arrow": "1.2.3", "eslint-plugin-react-hooks": "4.6.0", - "expect-playwright": "0.3.3", "fast-glob": "3.2.5", "fs-extra": "7.0.0", "html-inline-css-webpack-plugin": "1.11.2", @@ -170,7 +170,6 @@ "html-webpack-plugin": "5.5.3", "jest": "26.6.3", "jest-canvas-mock": "2.3.1", - "jest-playwright-preset": "1.5.1", "jest-react-hooks-shallow": "1.5.1", "jest-trx-results-processor": "0.0.7", "less": "3.8.1", @@ -179,7 +178,6 @@ "mini-css-extract-plugin": "2.1.0", "monaco-editor-webpack-plugin": "7.1.0", "node-fetch": "2.6.7", - "playwright": "1.13.0", "prettier": "3.0.3", "process": "0.11.10", "querystring-es3": "0.2.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..074e7e5f5 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,53 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: 'test', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 3 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? 'blob' : 'html', + timeout: 10 * 60 * 1000, + use: { + actionTimeout: 5 * 60 * 1000, + trace: 'off', + video: 'off', + screenshot: 'on', + testIdAttribute: 'data-test', + contextOptions: { + ignoreHTTPSErrors: true, + }, + }, + + expect: { + timeout: 5 * 60 * 1000, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + webServer: { + command: 'npm run start', + url: 'https://127.0.0.1:1234/_ready', + timeout: 120 * 1000, + ignoreHTTPSErrors: true, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx b/src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx index 80a682dda..854818138 100644 --- a/src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx @@ -166,6 +166,7 @@ export class LegacyTreeNodeComponent extends React.Component< return (
) => this.onNodeClick(event, node)} onKeyPress={(event: React.KeyboardEvent) => this.onNodeKeyPress(event, node)} @@ -174,9 +175,9 @@ export class LegacyTreeNodeComponent extends React.Component< >
{this.renderCollapseExpandIcon(node)} {node.iconSrc && } @@ -264,7 +265,7 @@ export class LegacyTreeNodeComponent extends React.Component< onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }), contextualMenuItemAs: (props: IContextualMenuItemProps) => (
e.target.dispatchEvent(LegacyTreeNodeComponent.createClickEvent())} > diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx index 354046222..fb60aab2e 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx @@ -147,6 +147,7 @@ export const TreeNodeComponent: React.FC = ({ const treeItem = (
=> { if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) { setFormError( - `Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`, + `Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`, ); - logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`); + logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`); logConsoleError( - `Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`, + `Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`, ); return; } @@ -123,17 +124,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent {!formError && }
* - Confirm by typing the database id + Confirm by typing the {getDatabaseName()} id { @@ -149,7 +151,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent - What is the reason why you are deleting this database? + What is the reason why you are deleting this {getDatabaseName()}? = ( - Confirm by typing the database id + Confirm by typing the + Database + id - What is the reason why you are deleting this database? + What is the reason why you are deleting this + Database + ?