From 9f1cc4cd5c28e9241664174356649ff9a22662cf Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Wed, 18 Sep 2024 13:49:09 -0400 Subject: [PATCH 01/13] Force Mongo and Proxy users to switch to Mongo and Cassandra Proxy (#1971) * Force Mongo and Cassandra users to the new Proxies * npm run format --------- Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.ts | 8 -------- src/ConfigContext.ts | 4 ---- src/Explorer/Tables/TableDataClient.ts | 8 -------- src/Explorer/Tabs/Tabs.tsx | 14 +------------- 4 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index d3b24928a..f47885bd8 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -724,15 +724,7 @@ export function useMongoProxyEndpoint(api: string): boolean { MongoProxyEndpoints.Mooncake, ]; - 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) ); diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index f5a17bbef..1c2c4ab41 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -54,10 +54,8 @@ export interface ConfigContext { MONGO_BACKEND_ENDPOINT?: string; MONGO_PROXY_ENDPOINT: string; NEW_MONGO_APIS?: string[]; - MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean; CASSANDRA_PROXY_ENDPOINT: string; NEW_CASSANDRA_APIS?: string[]; - CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean; PROXY_PATH?: string; JUNO_ENDPOINT: string; GITHUB_CLIENT_ID: string; @@ -119,10 +117,8 @@ let configContext: Readonly = { "legacyMongoShell", // "bulkdelete", ], - MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"], - CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false, isTerminalEnabled: false, isPhoenixEnabled: false, }; diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 8ba912bc5..4e0859f95 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -757,15 +757,7 @@ export class CassandraAPIDataClient extends TableDataClient { CassandraProxyEndpoints.Mooncake, ]; - let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled"; - if ( - configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development && - userContext.databaseAccount.properties.ipRules?.length > 0 - ) { - canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED; - } return ( - canAccessCassandraProxy && configContext.NEW_CASSANDRA_APIS?.includes(api) && activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT) ); diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 2bf21d3ab..3ee50992c 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -1,7 +1,7 @@ import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react"; import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants"; import { sendMessage } from "Common/MessageHandler"; -import { configContext, updateConfigContext } from "ConfigContext"; +import { configContext } from "ConfigContext"; import { IpRule } from "Contracts/DataModels"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { CollectionTabKind } from "Contracts/ViewModels"; @@ -370,12 +370,6 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { ipAddressesFromIPRules.includes(mongoProxyOutboundIP), ); - if (ipRulesIncludeMongoProxy) { - updateConfigContext({ - MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true, - }); - } - return !ipRulesIncludeMongoProxy; } else if (userContext.apiType === "Cassandra") { const isProdOrMpacCassandraProxyEndpoint: boolean = [ @@ -394,12 +388,6 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { (cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP), ); - if (ipRulesIncludeCassandraProxy) { - updateConfigContext({ - CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true, - }); - } - return !ipRulesIncludeCassandraProxy; } } From 42a1c6c3195c9237d58620fbfd7680257127287a Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Thu, 19 Sep 2024 07:18:03 +0200 Subject: [PATCH 02/13] Move table column selection out of feature flag to MPAC. (#1973) --- .../DocumentsTableComponent.tsx | 41 ++++++++++--------- src/Platform/Hosted/extractFeatures.ts | 2 - 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx index c96d63ff5..36689dc59 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx @@ -38,6 +38,7 @@ import { TextSortDescendingRegular, } from "@fluentui/react-icons"; import { NormalizedEventKey } from "Common/Constants"; +import { Environment, getEnvironment } from "Common/EnvironmentUtility"; import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane"; import { ColumnSizesMap, @@ -50,7 +51,6 @@ import { import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper"; import { LayoutConstants } from "Explorer/Theme/ThemeUtil"; -import { userContext } from "UserContext"; import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils"; import { useSidePanel } from "hooks/useSidePanel"; import React, { useCallback, useMemo } from "react"; @@ -228,7 +228,7 @@ export const DocumentsTableComponent: React.FC = } onClick={onRefreshTable}> Refresh - {userContext.features.enableDocumentsTableColumnSelection && ( + {[Environment.Development, Environment.Mpac].includes(getEnvironment()) && ( <> } @@ -260,24 +260,25 @@ export const DocumentsTableComponent: React.FC = > Resize with left/right arrow keys - {userContext.features.enableDocumentsTableColumnSelection && !isColumnSelectionDisabled && ( - } - onClick={() => { - // Remove column id from selectedColumnIds - const index = selectedColumnIds.indexOf(column.id); - if (index === -1) { - return; - } - const newSelectedColumnIds = [...selectedColumnIds]; - newSelectedColumnIds.splice(index, 1); - onColumnSelectionChange(newSelectedColumnIds); - }} - > - Remove column - - )} + {[Environment.Development, Environment.Mpac].includes(getEnvironment()) && + !isColumnSelectionDisabled && ( + } + onClick={() => { + // Remove column id from selectedColumnIds + const index = selectedColumnIds.indexOf(column.id); + if (index === -1) { + return; + } + const newSelectedColumnIds = [...selectedColumnIds]; + newSelectedColumnIds.splice(index, 1); + onColumnSelectionChange(newSelectedColumnIds); + }} + > + Remove column + + )} diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 2ae14e59e..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 enableDocumentsTableColumnSelection: 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"), - enableDocumentsTableColumnSelection: "true" === get("enabledocumentstablecolumnselection"), }; } From 869d81dfbcbaf070ff10408ef5d16449e154b7f1 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:09:09 -0500 Subject: [PATCH 03/13] fix partitionkey value fetching (#1972) * fix partitionkey value fetching * fix partitionkey value fetching * added unit test * Added some unit tests * move the constant --- .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 5 +- src/Utils/QueryUtils.test.ts | 80 ++++++++++++++----- src/Utils/QueryUtils.ts | 28 ++++++- 3 files changed, 87 insertions(+), 26 deletions(-) diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 19314f005..741139e39 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -1025,7 +1025,10 @@ export const DocumentsTabComponent: React.FunctionComponent setSelectedRows(new Set([documentIds.length - 1]))) + .then(() => { + setSelectedRows(new Set([documentIds.length - 1])); + setClickedRowIndex(documentIds.length - 1); + }) .finally(() => setIsExecuting(false)); }, [ onExecutionErrorChange, diff --git a/src/Utils/QueryUtils.test.ts b/src/Utils/QueryUtils.test.ts index 699626569..ab2b2a8b1 100644 --- a/src/Utils/QueryUtils.test.ts +++ b/src/Utils/QueryUtils.test.ts @@ -4,7 +4,28 @@ import * as sinon from "sinon"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import * as QueryUtils from "./QueryUtils"; -import { defaultQueryFields, extractPartitionKeyValues } from "./QueryUtils"; +import { defaultQueryFields, extractPartitionKeyValues, getValueForPath } from "./QueryUtils"; + +const documentContent = { + "Volcano Name": "Adams", + Country: "United States", + Region: "US-Washington", + Location: { + type: "Point", + coordinates: [-121.49, 46.206], + }, + Elevation: 3742, + Type: "Stratovolcano", + Category: "", + Status: "Tephrochronology", + "Last Known Eruption": "Last known eruption from A.D. 1-1499, inclusive", + id: "9e3c494e-8367-3f50-1f56-8c6fcb961363", + _rid: "xzo0AJRYUxUFAAAAAAAAAA==", + _self: "dbs/xzo0AA==/colls/xzo0AJRYUxU=/docs/xzo0AJRYUxUFAAAAAAAAAA==/", + _etag: '"ce00fa43-0000-0100-0000-652840440000"', + _attachments: "attachments/", + _ts: 1697136708, +}; describe("Query Utils", () => { const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => { @@ -111,28 +132,30 @@ describe("Query Utils", () => { }); }); - describe("extractPartitionKey", () => { - const documentContent = { - "Volcano Name": "Adams", - Country: "United States", - Region: "US-Washington", - Location: { - type: "Point", - coordinates: [-121.49, 46.206], - }, - Elevation: 3742, - Type: "Stratovolcano", - Category: "", - Status: "Tephrochronology", - "Last Known Eruption": "Last known eruption from A.D. 1-1499, inclusive", - id: "9e3c494e-8367-3f50-1f56-8c6fcb961363", - _rid: "xzo0AJRYUxUFAAAAAAAAAA==", - _self: "dbs/xzo0AA==/colls/xzo0AJRYUxU=/docs/xzo0AJRYUxUFAAAAAAAAAA==/", - _etag: '"ce00fa43-0000-0100-0000-652840440000"', - _attachments: "attachments/", - _ts: 1697136708, - }; + describe("getValueForPath", () => { + it("should return the correct value for a simple path", () => { + const pathSegments = ["Volcano Name"]; + expect(getValueForPath(documentContent, pathSegments)).toBe("Adams"); + }); + it("should return the correct value for a nested path", () => { + const pathSegments = ["Location", "coordinates"]; + expect(getValueForPath(documentContent, pathSegments)).toEqual([-121.49, 46.206]); + }); + it("should return undefined for a non-existing path", () => { + const pathSegments = ["NonExistent", "Path"]; + expect(getValueForPath(documentContent, pathSegments)).toBeUndefined(); + }); + it("should return undefined for an invalid path", () => { + const pathSegments = ["Location", "InvalidKey"]; + expect(getValueForPath(documentContent, pathSegments)).toBeUndefined(); + }); + it("should return the root object if pathSegments is empty", () => { + const pathSegments: string[] = []; + expect(getValueForPath(documentContent, pathSegments)).toBeUndefined(); + }); + }); + describe("extractPartitionKey", () => { it("should extract single partition key value", () => { const singlePartitionKeyDefinition: PartitionKeyDefinition = { kind: PartitionKeyKind.Hash, @@ -189,5 +212,18 @@ describe("Query Utils", () => { ); expect(partitionKeyValues.length).toBe(0); }); + + it("should extract all partition key values for hierarchical and nested partition keys", () => { + const mixedPartitionKeyDefinition: PartitionKeyDefinition = { + kind: PartitionKeyKind.MultiHash, + paths: ["/Country", "/Location/type"], + }; + const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues( + documentContent, + mixedPartitionKeyDefinition, + ); + expect(partitionKeyValues.length).toBe(2); + expect(partitionKeyValues).toEqual(["United States", "Point"]); + }); }); }); diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index 3cb3979d3..f0b39e4e2 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -95,6 +95,24 @@ export const queryPagesUntilContentPresent = async ( return await doRequest(firstItemIndex); }; +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const getValueForPath = (content: any, pathSegments: string[]): any => { + if (pathSegments.length === 0) { + return undefined; + } + + let currentValue = content; + + for (const segment of pathSegments) { + if (!currentValue || currentValue[segment] === undefined) { + return undefined; + } + currentValue = currentValue[segment]; + } + + return currentValue; +}; + /* eslint-disable @typescript-eslint/no-explicit-any */ export const extractPartitionKeyValues = ( documentContent: any, @@ -105,11 +123,15 @@ export const extractPartitionKeyValues = ( } const partitionKeyValues: PartitionKey[] = []; + partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => { - const partitionKeyPathWithoutSlash: string = partitionKeyPath.substring(1); - if (documentContent[partitionKeyPathWithoutSlash] !== undefined) { - partitionKeyValues.push(documentContent[partitionKeyPathWithoutSlash]); + const pathSegments: string[] = partitionKeyPath.substring(1).split("/"); + const value = getValueForPath(documentContent, pathSegments); + + if (value !== undefined) { + partitionKeyValues.push(value); } }); + return partitionKeyValues; }; From 23b2e5956054afba1f9b8b357d12b6071660c557 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Fri, 20 Sep 2024 08:26:58 +0200 Subject: [PATCH 04/13] Migrate Most Recent activity local storage to App State persistence (#1967) * Rewrite MostRecentActivity to leverage AppStatePersistenceUtility. * Fix format. Update type enum. * Migrate Item enum to string enum * Fix unit tests * Fix build issue --- .../MostRecentActivity.test.ts | 80 ++--- .../MostRecentActivity/MostRecentActivity.ts | 302 +++++++++--------- src/Explorer/SplashScreen/SplashScreen.tsx | 4 +- src/Explorer/Tree/ResourceTreeAdapter.tsx | 4 +- src/Explorer/Tree/treeNodeUtil.tsx | 8 +- src/Shared/AppStatePersistenceUtility.ts | 6 + src/Shared/StorageUtility.ts | 2 +- 7 files changed, 197 insertions(+), 209 deletions(-) diff --git a/src/Explorer/MostRecentActivity/MostRecentActivity.test.ts b/src/Explorer/MostRecentActivity/MostRecentActivity.test.ts index a8e84f3a2..db22eeb86 100644 --- a/src/Explorer/MostRecentActivity/MostRecentActivity.test.ts +++ b/src/Explorer/MostRecentActivity/MostRecentActivity.test.ts @@ -1,13 +1,13 @@ +import { clear, collectionWasOpened, getItems, Type } from "Explorer/MostRecentActivity/MostRecentActivity"; import { observable } from "knockout"; -import { mostRecentActivity } from "./MostRecentActivity"; describe("MostRecentActivity", () => { - const accountId = "some account"; + const accountName = "some account"; - beforeEach(() => mostRecentActivity.clear(accountId)); + beforeEach(() => clear(accountName)); it("Has no items at first", () => { - expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]); + expect(getItems(accountName)).toStrictEqual([]); }); it("Can record collections being opened", () => { @@ -18,9 +18,9 @@ describe("MostRecentActivity", () => { databaseId, }; - mostRecentActivity.collectionWasOpened(accountId, collection); + collectionWasOpened(accountName, collection); - const activity = mostRecentActivity.getItems(accountId); + const activity = getItems(accountName); expect(activity).toEqual([ expect.objectContaining({ collectionId, @@ -29,58 +29,24 @@ describe("MostRecentActivity", () => { ]); }); - it("Can record notebooks being opened", () => { - const name = "some notebook"; - const path = "some path"; - const notebook = { name, path }; + it("Does not store duplicate entries", () => { + const collectionId = "some collection"; + const databaseId = "some database"; + const collection = { + id: observable(collectionId), + databaseId, + }; - mostRecentActivity.notebookWasItemOpened(accountId, notebook); + collectionWasOpened(accountName, collection); + collectionWasOpened(accountName, collection); - const activity = mostRecentActivity.getItems(accountId); - expect(activity).toEqual([expect.objectContaining(notebook)]); - }); - - it("Filters out duplicates", () => { - const name = "some notebook"; - const path = "some path"; - const notebook = { name, path }; - const sameNotebook = { name, path }; - - mostRecentActivity.notebookWasItemOpened(accountId, notebook); - mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook); - - const activity = mostRecentActivity.getItems(accountId); - expect(activity.length).toEqual(1); - expect(activity).toEqual([expect.objectContaining(notebook)]); - }); - - it("Allows for multiple accounts", () => { - const name = "some notebook"; - const path = "some path"; - const notebook = { name, path }; - - const anotherNotebook = { name: "Another " + name, path }; - const anotherAccountId = "Another " + accountId; - - mostRecentActivity.notebookWasItemOpened(accountId, notebook); - mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook); - - expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]); - expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]); - }); - - it("Can store multiple distinct elements, in FIFO order", () => { - const name = "some notebook"; - const path = "some path"; - const first = { name, path }; - const second = { name: "Another " + name, path }; - const third = { name, path: "Another " + path }; - - mostRecentActivity.notebookWasItemOpened(accountId, first); - mostRecentActivity.notebookWasItemOpened(accountId, second); - mostRecentActivity.notebookWasItemOpened(accountId, third); - - const activity = mostRecentActivity.getItems(accountId); - expect(activity).toEqual([third, second, first].map(expect.objectContaining)); + const activity = getItems(accountName); + expect(activity).toEqual([ + expect.objectContaining({ + type: Type.OpenCollection, + collectionId, + databaseId, + }), + ]); }); }); diff --git a/src/Explorer/MostRecentActivity/MostRecentActivity.ts b/src/Explorer/MostRecentActivity/MostRecentActivity.ts index 0e77cf690..18847338c 100644 --- a/src/Explorer/MostRecentActivity/MostRecentActivity.ts +++ b/src/Explorer/MostRecentActivity/MostRecentActivity.ts @@ -1,10 +1,10 @@ +import { AppStateComponentNames, deleteState, loadState, saveState } from "Shared/AppStatePersistenceUtility"; import { CollectionBase } from "../../Contracts/ViewModels"; -import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility"; -import { NotebookContentItem } from "../Notebook/NotebookContentItem"; +import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; export enum Type { - OpenCollection, - OpenNotebook, + OpenCollection = "OpenCollection", + OpenNotebook = "OpenNotebook", } export interface OpenNotebookItem { @@ -21,158 +21,174 @@ export interface OpenCollectionItem { type Item = OpenNotebookItem | OpenCollectionItem; -// Update schemaVersion if you are going to change this interface -interface StoredData { - schemaVersion: string; - itemsMap: { [accountId: string]: Item[] }; // FIFO -} +const itemsMaxNumber: number = 5; /** - * Stores most recent activity + * Migrate old data to new AppState */ -class MostRecentActivity { - private static readonly schemaVersion: string = "2"; - private static itemsMaxNumber: number = 5; - private storedData: StoredData; - constructor() { - // Retrieve from local storage - if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) { - const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity); - - if (!rawData) { - this.storedData = MostRecentActivity.createEmptyData(); - } else { - try { - this.storedData = JSON.parse(rawData); - } catch (e) { - console.error("Unable to parse stored most recent activity. Use empty data:", rawData); - this.storedData = MostRecentActivity.createEmptyData(); - } - - // If version doesn't match or schema broke, nuke it! - if ( - !this.storedData.hasOwnProperty("schemaVersion") || - this.storedData["schemaVersion"] !== MostRecentActivity.schemaVersion - ) { - LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity); - this.storedData = MostRecentActivity.createEmptyData(); - } +const migrateOldData = () => { + if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) { + const oldDataSchemaVersion: string = "2"; + const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity); + if (rawData) { + const oldData = JSON.parse(rawData); + if (oldData.schemaVersion === oldDataSchemaVersion) { + const itemsMap: Record = oldData.itemsMap; + Object.keys(itemsMap).forEach((accountId: string) => { + const accountName = accountId.split("/").pop(); + if (accountName) { + saveState( + { + componentName: AppStateComponentNames.MostRecentActivity, + globalAccountName: accountName, + }, + itemsMap[accountId].map((item) => { + if ((item.type as unknown as number) === 0) { + item.type = Type.OpenCollection; + } else if ((item.type as unknown as number) === 1) { + item.type = Type.OpenNotebook; + } + return item; + }), + ); + } + }); } - } else { - this.storedData = MostRecentActivity.createEmptyData(); } - for (let p in this.storedData.itemsMap) { - this.cleanupItems(p); + // Remove old data + LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity); + } +}; + +const addItem = (accountName: string, newItem: Item): void => { + // When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable. + // if (!accountId) { + // return; + // } + + let items = + (loadState({ + componentName: AppStateComponentNames.MostRecentActivity, + globalAccountName: accountName, + }) as Item[]) || []; + + // Remove duplicate + items = removeDuplicate(newItem, items); + + items.unshift(newItem); + items = cleanupItems(items, accountName); + saveState( + { + componentName: AppStateComponentNames.MostRecentActivity, + globalAccountName: accountName, + }, + items, + ); +}; + +export const getItems = (accountName: string): Item[] => { + if (!accountName) { + return []; + } + + return ( + (loadState({ + componentName: AppStateComponentNames.MostRecentActivity, + globalAccountName: accountName, + }) as Item[]) || [] + ); +}; + +export const collectionWasOpened = ( + accountName: string, + { id, databaseId }: Pick, +) => { + if (accountName === undefined) { + return; + } + + const collectionId = id(); + addItem(accountName, { + type: Type.OpenCollection, + databaseId, + collectionId, + }); +}; + +export const clear = (accountName: string): void => { + if (!accountName) { + return; + } + + deleteState({ + componentName: AppStateComponentNames.MostRecentActivity, + globalAccountName: accountName, + }); +}; + +// Sort object by key +const sortObjectKeys = (unordered: Record): Record => { + return Object.keys(unordered) + .sort() + .reduce((obj: Record, key: string) => { + obj[key] = unordered[key]; + return obj; + }, {}); +}; + +/** + * Find items by doing strict comparison and remove from array if duplicate is found. + * Modifies the array. + * @param item + * @param itemsArray + * @returns new array + */ +const removeDuplicate = (item: Item, itemsArray: Item[]): Item[] => { + if (!itemsArray) { + return itemsArray; + } + + const result: Item[] = [...itemsArray]; + + let index = -1; + for (let i = 0; i < result.length; i++) { + const currentItem = result[i]; + + if ( + JSON.stringify(sortObjectKeys(currentItem as unknown as Record)) === + JSON.stringify(sortObjectKeys(item as unknown as Record)) + ) { + index = i; + break; } - this.saveToLocalStorage(); } - private static createEmptyData(): StoredData { - return { - schemaVersion: MostRecentActivity.schemaVersion, - itemsMap: {}, - }; + if (index !== -1) { + result.splice(index, 1); } - private static isEmpty(object: any) { - return Object.keys(object).length === 0 && object.constructor === Object; + return result; +}; + +/** + * Remove unknown types + * Limit items to max number + * Modifies the array. + */ +const cleanupItems = (items: Item[], accountName: string): Item[] => { + if (accountName === undefined) { + return []; } - private saveToLocalStorage() { - if (MostRecentActivity.isEmpty(this.storedData.itemsMap)) { - if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) { - LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity); - } - // Don't save if empty - return; - } - - LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData)); - } - - private addItem(accountId: string, newItem: Item): void { - // When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable. - // if (!accountId) { - // return; - // } - - // Remove duplicate - MostRecentActivity.removeDuplicate(newItem, this.storedData.itemsMap[accountId]); - - this.storedData.itemsMap[accountId] = this.storedData.itemsMap[accountId] || []; - this.storedData.itemsMap[accountId].unshift(newItem); - this.cleanupItems(accountId); - this.saveToLocalStorage(); - } - - public getItems(accountId: string): Item[] { - return this.storedData.itemsMap[accountId] || []; - } - - public collectionWasOpened(accountId: string, { id, databaseId }: Pick) { - const collectionId = id(); - this.addItem(accountId, { - type: Type.OpenCollection, - databaseId, - collectionId, + const itemsArray = items.filter((item) => item.type in Type).slice(0, itemsMaxNumber); + if (itemsArray.length === 0) { + deleteState({ + componentName: AppStateComponentNames.MostRecentActivity, + globalAccountName: accountName, }); } + return itemsArray; +}; - public notebookWasItemOpened(accountId: string, { name, path }: Pick) { - this.addItem(accountId, { - type: Type.OpenNotebook, - name, - path, - }); - } - - public clear(accountId: string): void { - delete this.storedData.itemsMap[accountId]; - this.saveToLocalStorage(); - } - - /** - * Find items by doing strict comparison and remove from array if duplicate is found - * @param item - */ - private static removeDuplicate(item: Item, itemsArray: Item[]): void { - if (!itemsArray) { - return; - } - - let index = -1; - for (let i = 0; i < itemsArray.length; i++) { - const currentItem = itemsArray[i]; - if (JSON.stringify(currentItem) === JSON.stringify(item)) { - index = i; - break; - } - } - - if (index !== -1) { - itemsArray.splice(index, 1); - } - } - - /** - * Remove unknown types - * Limit items to max number - */ - private cleanupItems(accountId: string): void { - if (!this.storedData.itemsMap.hasOwnProperty(accountId)) { - return; - } - - const itemsArray = this.storedData.itemsMap[accountId] - .filter((item) => item.type in Type) - .slice(0, MostRecentActivity.itemsMaxNumber); - if (itemsArray.length === 0) { - delete this.storedData.itemsMap[accountId]; - } else { - this.storedData.itemsMap[accountId] = itemsArray; - } - } -} - -export const mostRecentActivity = new MostRecentActivity(); +migrateOldData(); diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index e3de8c503..495bc03ee 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -114,7 +114,7 @@ export class SplashScreen extends React.Component { } private clearMostRecent = (): void => { - MostRecentActivity.mostRecentActivity.clear(userContext.databaseAccount?.id); + MostRecentActivity.clear(userContext.databaseAccount?.name); this.setState({}); }; @@ -498,7 +498,7 @@ export class SplashScreen extends React.Component { } private createRecentItems(): SplashScreenItem[] { - return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => { + return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => { switch (activity.type) { default: { const unknownActivity: never = activity; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 3e669a1b2..76d3e9308 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,4 +1,5 @@ import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; +import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil"; import { getItemName } from "Utils/APITypeUtils"; import * as ko from "knockout"; @@ -28,7 +29,6 @@ import { useDialog } from "../Controls/Dialog"; import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent"; 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"; @@ -229,7 +229,7 @@ export class ResourceTreeAdapter implements ReactAdapter { onClick: () => { collection.openTab(); // push to most recent - mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); + collectionWasOpened(userContext.databaseAccount?.name, collection); }, isSelected: () => useSelectedNode diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index b6ec04e01..8e6c94559 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -1,5 +1,6 @@ import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; +import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import TabsBase from "Explorer/Tabs/TabsBase"; import StoredProcedure from "Explorer/Tree/StoredProcedure"; import Trigger from "Explorer/Tree/Trigger"; @@ -17,7 +18,6 @@ import { userContext } from "../../UserContext"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; -import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity"; import { useNotebook } from "../Notebook/useNotebook"; import { useSelectedNode } from "../useSelectedNode"; @@ -98,7 +98,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa onClick: () => { collection.onDocumentDBDocumentsClick(); // push to most recent - mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); + collectionWasOpened(userContext.databaseAccount?.name, collection); }, isSelected: () => useSelectedNode @@ -234,7 +234,7 @@ export const buildCollectionNode = ( useSelectedNode.getState().setSelectedNode(collection); collection.openTab(); // push to most recent - mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); + collectionWasOpened(userContext.databaseAccount?.name, collection); }, onExpanded: async () => { // Rewritten version of expandCollapseCollection @@ -282,7 +282,7 @@ const buildCollectionNodeChildren = ( onClick: () => { collection.openTab(); // push to most recent - mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); + collectionWasOpened(userContext.databaseAccount?.name, collection); }, isSelected: () => useSelectedNode diff --git a/src/Shared/AppStatePersistenceUtility.ts b/src/Shared/AppStatePersistenceUtility.ts index 3d65ff0a7..b48258d4f 100644 --- a/src/Shared/AppStatePersistenceUtility.ts +++ b/src/Shared/AppStatePersistenceUtility.ts @@ -3,6 +3,7 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; // The component name whose state is being saved. Component name must not include special characters. export enum AppStateComponentNames { DocumentsTab = "DocumentsTab", + MostRecentActivity = "MostRecentActivity", QueryCopilot = "QueryCopilot", } @@ -34,6 +35,7 @@ export const loadState = (path: StorePath): unknown => { const key = createKeyFromPath(path); return appState[key]?.data; }; + export const saveState = (path: StorePath, state: unknown): void => { // Retrieve state object const appState = @@ -65,6 +67,10 @@ export const deleteState = (path: StorePath): void => { LocalStorageUtility.setEntryObject(StorageKey.AppState, appState); }; +export const hasState = (path: StorePath): boolean => { + return loadState(path) !== undefined; +}; + // This is for high-frequency state changes let timeoutId: NodeJS.Timeout | undefined; export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => { diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index 7a55513ed..f2baa85da 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -24,7 +24,7 @@ export enum StorageKey { MaxDegreeOfParellism, IsGraphAutoVizDisabled, TenantId, - MostRecentActivity, + MostRecentActivity, // deprecated SetPartitionKeyUndefined, GalleryCalloutDismissed, VisitedAccounts, From 053dc9d76bd84893d122e3f1cf08e2bfe16e0a21 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Fri, 20 Sep 2024 08:28:03 -0700 Subject: [PATCH 05/13] Add config files for Codespaces (#1975) --- .devcontainer/Dockerfile | 16 ++++++++++++++++ .devcontainer/devcontainer.json | 32 ++++++++++++++++++++++++++++++++ .devcontainer/oncreate | 4 ++++ 3 files changed, 52 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/oncreate diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..2df2e3e81 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm + +# Install pre-reqs for gyp, and 'canvas' npm module +RUN apt-get update && \ + apt-get install -y \ + make \ + gcc \ + g++ \ + python3-minimal \ + libcairo2-dev \ + libpango1.0-dev \ + && \ + rm -rf /var/lib/apt/lists/* + +# Install node-gyp to build native modules +RUN npm install -g node-gyp \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..b6d399f2c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Azure Cosmos DB Explorer", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + "dockerfile": "Dockerfile" + }, + "onCreateCommand": ".devcontainer/oncreate", + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "installDirectlyFromGitHubRelease": true, + "version": "latest" + }, + "ghcr.io/devcontainers/features/sshd:1": { + "version": "latest" + } + } + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/.devcontainer/oncreate b/.devcontainer/oncreate new file mode 100755 index 000000000..6ce3fba21 --- /dev/null +++ b/.devcontainer/oncreate @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# Install packages once, to prime the node_modules directory. +npm ci \ No newline at end of file From 7128133874ae5379501a0691d2741b49b955ef73 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Mon, 23 Sep 2024 17:30:42 +0200 Subject: [PATCH 06/13] Only show throttling warning when throttling happened. (#1976) --- .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 741139e39..c5445c2f9 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -645,6 +645,7 @@ export const DocumentsTabComponent: React.FunctionComponent(undefined); const [bulkDeleteOperation, setBulkDeleteOperation] = useState<{ onCompleted: (documentIds: DocumentId[]) => void; @@ -754,6 +755,7 @@ export const DocumentsTabComponent: React.FunctionComponent 0, })); }) .catch((error) => { @@ -764,6 +766,7 @@ export const DocumentsTabComponent: React.FunctionComponent )} - - - Warning - {get429WarningMessageNoSql()}{" "} - - Learn More - - - + {bulkDeleteProcess.hasBeenThrottled && ( + + + Warning + {get429WarningMessageNoSql()}{" "} + + Learn More + + + + )} )} From 1a4f713a792f6c29dae7f4be1d14de4f3df426b4 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Mon, 23 Sep 2024 11:34:49 -0700 Subject: [PATCH 07/13] Clarifying copy-edit to delete database panel (#1974) * change 'Database id' to 'Database name' in Delete Database confirm prompt * put 'name' in a parenthetical instead of replacing 'id' * update test snapshots --- src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx | 4 ++-- .../DeleteDatabaseConfirmationPanel.test.tsx.snap | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx index af291bba6..04bc117ca 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx @@ -124,7 +124,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent @@ -132,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent
* - Confirm by typing the {getDatabaseName()} id + {confirmDatabase} - Confirm by typing the - Database - id + Confirm by typing the Database id (name) Date: Wed, 25 Sep 2024 09:15:54 -0700 Subject: [PATCH 08/13] Ensure the "Ctrl+Alt+["/"Ctrl+Alt+]" shortcuts don't get triggered on "AltGr+8"/"AltGr+9" (#1979) * Remove the "Ctrl+Alt+[" and "Ctrl+Alt+]" shortcuts, as they conflict on non-US keyboard layouts * Use "BracketLeft" and "BracketRight" to re-enable shortcut for US keyboards --- src/KeyboardShortcuts.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/KeyboardShortcuts.tsx b/src/KeyboardShortcuts.tsx index 96e7181a3..b155f0483 100644 --- a/src/KeyboardShortcuts.tsx +++ b/src/KeyboardShortcuts.tsx @@ -83,8 +83,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+[", "$mod+Shift+F6"], - [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"], + [KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+BracketLeft", "$mod+Shift+F6"], + [KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+BracketRight", "$mod+F6"], [KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"], [KeyboardAction.SEARCH]: ["$mod+Shift+F"], [KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"], From e0b773d920c1a09c9db22d499c804d047eaa4055 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Fri, 27 Sep 2024 09:59:41 -0700 Subject: [PATCH 09/13] Set shared throughput default to false for New Databases (#1981) * Introduce common function for shared throughput default and set to false. * Add new file. * Adjust E2E tests to not set throughput for database create. --- src/Common/DatabaseUtility.ts | 3 +++ src/Explorer/Panes/AddCollectionPanel.tsx | 8 ++------ .../AddDatabasePanel/AddDatabasePanel.tsx | 3 ++- .../AddDatabasePanel.test.tsx.snap | 10 +--------- .../AddCollectionPanel.test.tsx.snap | 18 +++++++++--------- test/cassandra/container.spec.ts | 1 - test/gremlin/container.spec.ts | 1 - test/mongo/container.spec.ts | 1 - test/sql/container.spec.ts | 1 - 9 files changed, 17 insertions(+), 29 deletions(-) create mode 100644 src/Common/DatabaseUtility.ts diff --git a/src/Common/DatabaseUtility.ts b/src/Common/DatabaseUtility.ts new file mode 100644 index 000000000..c5b8538ff --- /dev/null +++ b/src/Common/DatabaseUtility.ts @@ -0,0 +1,3 @@ +export function getNewDatabaseSharedThroughputDefault(): boolean { + return false; +} diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index a3a01d5fb..d007f7734 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -17,10 +17,10 @@ import { } from "@fluentui/react"; import * as Constants from "Common/Constants"; import { createCollection } from "Common/dataAccess/createCollection"; +import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { configContext, Platform } from "ConfigContext"; import * as DataModels from "Contracts/DataModels"; -import { SubscriptionType } from "Contracts/SubscriptionType"; import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm"; import { useSidePanel } from "hooks/useSidePanel"; import { useTeachingBubble } from "hooks/useTeachingBubble"; @@ -125,7 +125,7 @@ export class AddCollectionPanel extends React.Component = ({ const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState( - subscriptionType !== SubscriptionType.EA && !isServerlessAccount(), + getNewDatabaseSharedThroughputDefault(), ); const [formErrors, setFormErrors] = useState(""); const [isExecuting, setIsExecuting] = useState(false); diff --git a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap index 7c5f3e6c7..68a21f696 100644 --- a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap +++ b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap @@ -65,7 +65,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = ` horizontal={true} > -
`; diff --git a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap index 8574aa465..fa2ae5a76 100644 --- a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap @@ -106,7 +106,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = ` horizontal={true} > - + { async (panel, okButton) => { await panel.getByPlaceholder("Type a new keyspace id").fill(keyspaceId); await panel.getByPlaceholder("Enter table Id").fill(tableId); - await panel.getByLabel("Table max RU/s").fill("1000"); await okButton.click(); }, { closeTimeout: 5 * 60 * 1000 }, diff --git a/test/gremlin/container.spec.ts b/test/gremlin/container.spec.ts index 7c6d066b7..f2efdb214 100644 --- a/test/gremlin/container.spec.ts +++ b/test/gremlin/container.spec.ts @@ -16,7 +16,6 @@ test("Gremlin graph CRUD", async ({ page }) => { await panel.getByPlaceholder("Type a new database id").fill(databaseId); await panel.getByRole("textbox", { name: "Graph id, Example Graph1" }).fill(graphId); await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); - await panel.getByLabel("Database max RU/s").fill("1000"); await okButton.click(); }, { closeTimeout: 5 * 60 * 1000 }, diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 2a3a4d991..a896d4656 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -21,7 +21,6 @@ import { DataExplorer, TestAccount, generateUniqueName } from "../fx"; await panel.getByPlaceholder("Type a new database id").fill(databaseId); await panel.getByRole("textbox", { name: "Collection id, Example Collection1" }).fill(collectionId); await panel.getByRole("textbox", { name: "Shard key" }).fill("pk"); - await panel.getByLabel("Database max RU/s").fill("1000"); await okButton.click(); }, { closeTimeout: 5 * 60 * 1000 }, diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index 7061bb09f..2fbad2462 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -15,7 +15,6 @@ test("SQL database and container CRUD", async ({ page }) => { await panel.getByPlaceholder("Type a new database id").fill(databaseId); await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); - await panel.getByLabel("Database max RU/s").fill("1000"); await okButton.click(); }, { closeTimeout: 5 * 60 * 1000 }, From cbcb7e624026625461b253895327afb96ff6b381 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 30 Sep 2024 07:29:24 -0700 Subject: [PATCH 10/13] Switch E2E tests to use new accounts. (#1982) --- test/fx.ts | 14 +++++++------- utils/cleanupDBs.js | 2 +- webpack.config.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/fx.ts b/test/fx.ts index cfd9aa5e6..b44028dc8 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -40,15 +40,15 @@ export enum TestAccount { } export const defaultAccounts: Record = { - [TestAccount.Tables]: "portal-tables-runner", - [TestAccount.Cassandra]: "portal-cassandra-runner", - [TestAccount.Gremlin]: "portal-gremlin-runner", - [TestAccount.Mongo]: "portal-mongo-runner", - [TestAccount.Mongo32]: "portal-mongo32-runner", - [TestAccount.SQL]: "portal-sql-runner-west-us", + [TestAccount.Tables]: "github-e2etests-tables", + [TestAccount.Cassandra]: "github-e2etests-cassandra", + [TestAccount.Gremlin]: "github-e2etests-gremlin", + [TestAccount.Mongo]: "github-e2etests-mongo", + [TestAccount.Mongo32]: "github-e2etests-mongo32", + [TestAccount.SQL]: "github-e2etests-sql", }; -export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "runners"; +export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests"; export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c"; function tryGetStandardName(accountType: TestAccount) { diff --git a/utils/cleanupDBs.js b/utils/cleanupDBs.js index b2bbf0be8..be1e1dc0d 100644 --- a/utils/cleanupDBs.js +++ b/utils/cleanupDBs.js @@ -3,7 +3,7 @@ const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const ms = require("ms"); const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"]; -const resourceGroupName = "runners"; +const resourceGroupName = "de-e2e-tests"; const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); diff --git a/webpack.config.js b/webpack.config.js index 16fa3dbab..043188275 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -23,7 +23,7 @@ const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8"); const AZURE_CLIENT_ID = "fd8753b0-0707-4e32-84e9-2532af865fb4"; const AZURE_TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47"; const SUBSCRIPTION_ID = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; -const RESOURCE_GROUP = "runners"; +const RESOURCE_GROUP = "de-e2e-tests"; const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET || process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET; // TODO Remove. Exists for backwards compat with old .env files. Prefer AZURE_CLIENT_SECRET if (!AZURE_CLIENT_SECRET) { From fae4589427d4d625eb351c0159f58e20da858acc Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Mon, 30 Sep 2024 11:40:48 -0400 Subject: [PATCH 11/13] Bulk Delete API fix (#1977) * Bulk Delete API fix * Bulk Delete API fix --------- Co-authored-by: Asier Isayas --- src/Common/MongoProxyClient.ts | 5 ++++- src/ConfigContext.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index f47885bd8..552d9f08d 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -561,7 +561,10 @@ export function deleteDocuments( const { databaseAccount } = userContext; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; - const rids = documentIds.map((documentId) => documentId.id()); + const rids: string[] = documentIds.map((documentId) => { + const idComponents = documentId.self.split("/"); + return idComponents[5]; + }); const params = { databaseID: databaseId, diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 1c2c4ab41..86fd92586 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -115,7 +115,7 @@ let configContext: Readonly = { "deleteDocument", "createCollectionWithProxy", "legacyMongoShell", - // "bulkdelete", + "bulkdelete", ], CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"], From 261289b0310f35ab38fb385edd2c81ccb4745fe2 Mon Sep 17 00:00:00 2001 From: Asier Isayas Date: Mon, 30 Sep 2024 14:34:37 -0400 Subject: [PATCH 12/13] Remove legacy backend references in tests and local dev (#1983) * remove legacy backend references in tests and local dev * fix unit tests * fixed bulk delete * fix tests * fix cosmosclient --------- Co-authored-by: Asier Isayas --- README.md | 2 +- docs/index.html | 2 +- preview/index.js | 4 +- src/Common/Constants.ts | 12 ++ src/Common/CosmosClient.test.ts | 13 +- src/Common/MongoProxyClient.test.ts | 62 +++++----- src/Common/MongoProxyClient.ts | 111 ++++++++++++++---- src/ConfigContext.ts | 13 +- .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 2 +- .../MongoShellTab/MongoShellTabComponent.tsx | 2 +- src/Utils/MessageValidation.test.ts | 6 +- src/Utils/NetworkUtility.test.ts | 10 +- src/Utils/NetworkUtility.ts | 9 +- webpack.config.js | 4 +- 14 files changed, 153 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index f82af79ee..797ab6c2b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Run `npm start` to start the development server and automatically rebuild on cha ### Hosted Development (https://cosmos.azure.com) - Visit: `https://localhost:1234/hostedExplorer.html` -- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine. +- The default webpack dev server configuration will proxy requests to the production portal backend: `https://cdb-ms-mpac-pbe.cosmos.azure.com`. This will allow you to use production connection strings on your local machine. ### Emulator Development diff --git a/docs/index.html b/docs/index.html index f62e4c4f2..7f71f301d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -82,7 +82,7 @@
  • Visit: https://localhost:1234/hostedExplorer.html
  • -
  • The default webpack dev server configuration will proxy requests to the production portal backend: https://main.documentdb.ext.azure.com. This will allow you to use production connection strings on your local machine.
  • +
  • The default webpack dev server configuration will proxy requests to the production portal backend: https://cdb-ms-mpac-pbe.cosmos.azure.com. This will allow you to use production connection strings on your local machine.

Emulator Development

diff --git a/preview/index.js b/preview/index.js index c205600a9..f38fae27c 100644 --- a/preview/index.js +++ b/preview/index.js @@ -4,7 +4,7 @@ const port = process.env.PORT || 3000; const fetch = require("node-fetch"); const api = createProxyMiddleware("/api", { - target: "https://main.documentdb.ext.azure.com", + target: "https://cdb-ms-mpac-pbe.cosmos.azure.com", changeOrigin: true, logLevel: "debug", bypass: (req, res) => { @@ -16,7 +16,7 @@ const api = createProxyMiddleware("/api", { }); const proxy = createProxyMiddleware("/proxy", { - target: "https://main.documentdb.ext.azure.com", + target: "https://cdb-ms-mpac-pbe.cosmos.azure.com", changeOrigin: true, secure: false, logLevel: "debug", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index d0d3837c2..23155cbdf 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -155,6 +155,18 @@ export class MongoProxyEndpoints { public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; } +export class MongoProxyApi { + public static readonly ResourceList: string = "ResourceList"; + public static readonly QueryDocuments: string = "QueryDocuments"; + public static readonly CreateDocument: string = "CreateDocumen"; + public static readonly ReadDocument: string = "ReadDocument"; + public static readonly UpdateDocument: string = "UpdateDocument"; + public static readonly DeleteDocument: string = "DeleteDocument"; + public static readonly CreateCollectionWithProxy: string = "CreateCollectionWithProxy"; + public static readonly LegacyMongoShell: string = "LegacyMongoShell"; + public static readonly BulkDelete: string = "BulkDelete"; +} + export class CassandraProxyEndpoints { public static readonly Development: string = "https://localhost:7240"; public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com"; diff --git a/src/Common/CosmosClient.test.ts b/src/Common/CosmosClient.test.ts index 3f5ff9f2a..c5c067f6c 100644 --- a/src/Common/CosmosClient.test.ts +++ b/src/Common/CosmosClient.test.ts @@ -1,4 +1,5 @@ -import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext"; +import { PortalBackendEndpoints } from "Common/Constants"; +import { configContext, Platform, resetConfigContext, updateConfigContext } from "../ConfigContext"; import { updateUserContext } from "../UserContext"; import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient"; @@ -20,22 +21,22 @@ describe("getTokenFromAuthService", () => { it("builds the correct URL in production", () => { updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, }); getTokenFromAuthService("GET", "dbs", "foo"); expect(window.fetch).toHaveBeenCalledWith( - "https://main.documentdb.ext.azure.com/api/guest/runtimeproxy/authorizationTokens", + `${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`, expect.any(Object), ); }); it("builds the correct URL in dev", () => { updateConfigContext({ - BACKEND_ENDPOINT: "https://localhost:1234", + PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development, }); getTokenFromAuthService("GET", "dbs", "foo"); expect(window.fetch).toHaveBeenCalledWith( - "https://localhost:1234/api/guest/runtimeproxy/authorizationTokens", + `${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`, expect.any(Object), ); }); @@ -78,7 +79,7 @@ describe("requestPlugin", () => { const next = jest.fn(); updateConfigContext({ platform: Platform.Hosted, - BACKEND_ENDPOINT: "https://localhost:1234", + PORTAL_BACKEND_ENDPOINT: "https://localhost:1234", PROXY_PATH: "/proxy", }); const headers = {}; diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index a4c75a344..cda6eb882 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -1,5 +1,6 @@ +import { MongoProxyEndpoints } from "Common/Constants"; import { AuthType } from "../AuthType"; -import { resetConfigContext, updateConfigContext } from "../ConfigContext"; +import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext"; import { DatabaseAccount } from "../Contracts/DataModels"; import { Collection } from "../Contracts/ViewModels"; import DocumentId from "../Explorer/Tree/DocumentId"; @@ -71,7 +72,7 @@ describe("MongoProxyClient", () => { databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -82,16 +83,16 @@ describe("MongoProxyClient", () => { it("builds the correct URL", () => { queryDocuments(databaseId, collection, true, "{}"); expect(window.fetch).toHaveBeenCalledWith( - "https://main.documentdb.ext.azure.com/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`, expect.any(Object), ); }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" }); queryDocuments(databaseId, collection, true, "{}"); expect(window.fetch).toHaveBeenCalledWith( - "https://localhost:1234/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`, expect.any(Object), ); }); @@ -103,7 +104,7 @@ describe("MongoProxyClient", () => { databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -114,16 +115,16 @@ describe("MongoProxyClient", () => { it("builds the correct URL", () => { readDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( - "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, expect.any(Object), ); }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" }); readDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( - "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, expect.any(Object), ); }); @@ -135,7 +136,7 @@ describe("MongoProxyClient", () => { databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -146,16 +147,16 @@ describe("MongoProxyClient", () => { it("builds the correct URL", () => { readDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( - "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, expect.any(Object), ); }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" }); readDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( - "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, expect.any(Object), ); }); @@ -167,7 +168,7 @@ describe("MongoProxyClient", () => { databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -178,7 +179,7 @@ describe("MongoProxyClient", () => { it("builds the correct URL", () => { updateDocument(databaseId, collection, documentId, "{}"); expect(window.fetch).toHaveBeenCalledWith( - "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, expect.any(Object), ); }); @@ -187,7 +188,7 @@ describe("MongoProxyClient", () => { updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); updateDocument(databaseId, collection, documentId, "{}"); expect(window.fetch).toHaveBeenCalledWith( - "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, expect.any(Object), ); }); @@ -199,7 +200,7 @@ describe("MongoProxyClient", () => { databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -210,16 +211,16 @@ describe("MongoProxyClient", () => { it("builds the correct URL", () => { deleteDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( - "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, expect.any(Object), ); }); it("builds the correct proxy URL in development", () => { - updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); + updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234" }); deleteDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( - "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk", + `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, expect.any(Object), ); }); @@ -231,13 +232,13 @@ describe("MongoProxyClient", () => { databaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, }); }); it("returns a production endpoint", () => { - const endpoint = getEndpoint("https://main.documentdb.ext.azure.com"); - expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer"); + const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); + expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`); }); it("returns a development endpoint", () => { @@ -249,18 +250,19 @@ describe("MongoProxyClient", () => { updateUserContext({ authType: AuthType.EncryptedToken, }); - const endpoint = getEndpoint("https://main.documentdb.ext.azure.com"); - expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); + const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); + expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`); }); }); + describe("getFeatureEndpointOrDefault", () => { beforeEach(() => { resetConfigContext(); updateConfigContext({ - BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", + MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, }); const params = new URLSearchParams({ - "feature.mongoProxyEndpoint": "https://localhost:12901", + "feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod, "feature.mongoProxyAPIs": "readDocument|createDocument", }); const features = extractFeatures(params); @@ -272,12 +274,12 @@ describe("MongoProxyClient", () => { it("returns a local endpoint", () => { const endpoint = getFeatureEndpointOrDefault("readDocument"); - expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer"); + expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`); }); it("returns a production endpoint", () => { - const endpoint = getFeatureEndpointOrDefault("deleteDocument"); - expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer"); + const endpoint = getFeatureEndpointOrDefault("DeleteDocument"); + expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`); }); }); }); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 552d9f08d..775ac9b18 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -14,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId"; import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { userContext } from "../UserContext"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; -import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants"; +import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants"; import { MinimalQueryIterator } from "./IteratorUtilities"; import { sendMessage } from "./MessageHandler"; @@ -67,7 +67,7 @@ export function queryDocuments( query: string, continuationToken?: string, ): Promise { - if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) { + if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) { return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken); } @@ -89,7 +89,7 @@ export function queryDocuments( query, }; - const endpoint = getFeatureEndpointOrDefault("resourcelist") || ""; + const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || ""; const headers = { ...defaultHeaders, @@ -194,7 +194,7 @@ export function readDocument( collection: Collection, documentId: DocumentId, ): Promise { - if (!useMongoProxyEndpoint("readDocument")) { + if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) { return readDocument_ToBeDeprecated(databaseId, collection, documentId); } const { databaseAccount } = userContext; @@ -217,7 +217,7 @@ export function readDocument( : "", }; - const endpoint = getFeatureEndpointOrDefault("readDocument"); + const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument); return window .fetch(endpoint, { @@ -289,7 +289,7 @@ export function createDocument( partitionKeyProperty: string, documentContent: unknown, ): Promise { - if (!useMongoProxyEndpoint("createDocument")) { + if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) { return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent); } const { databaseAccount } = userContext; @@ -308,7 +308,7 @@ export function createDocument( documentContent: JSON.stringify(documentContent), }; - const endpoint = getFeatureEndpointOrDefault("createDocument"); + const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument); return window .fetch(`${endpoint}/createDocument`, { @@ -373,7 +373,7 @@ export function updateDocument( documentId: DocumentId, documentContent: string, ): Promise { - if (!useMongoProxyEndpoint("updateDocument")) { + if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) { return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent); } const { databaseAccount } = userContext; @@ -396,7 +396,7 @@ export function updateDocument( : "", documentContent, }; - const endpoint = getFeatureEndpointOrDefault("updateDocument"); + const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument); return window .fetch(endpoint, { @@ -464,7 +464,7 @@ export function updateDocument_ToBeDeprecated( } export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise { - if (!useMongoProxyEndpoint("deleteDocument")) { + if (!useMongoProxyEndpoint(MongoProxyApi.DeleteDocument)) { return deleteDocument_ToBeDeprecated(databaseId, collection, documentId); } const { databaseAccount } = userContext; @@ -486,7 +486,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum ? documentId.partitionKeyProperties?.[0] : "", }; - const endpoint = getFeatureEndpointOrDefault("deleteDocument"); + const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.DeleteDocument); return window .fetch(endpoint, { @@ -575,7 +575,7 @@ export function deleteDocuments( resourceGroup: userContext.resourceGroup, databaseAccountName: databaseAccount.name, }; - const endpoint = getFeatureEndpointOrDefault("bulkdelete"); + const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete); return window .fetch(`${endpoint}/bulkdelete`, { @@ -599,7 +599,7 @@ export function deleteDocuments( export function createMongoCollectionWithProxy( params: DataModels.CreateCollectionParams, ): Promise { - if (!useMongoProxyEndpoint("createCollectionWithProxy")) { + if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) { return createMongoCollectionWithProxy_ToBeDeprecated(params); } const { databaseAccount } = userContext; @@ -622,7 +622,7 @@ export function createMongoCollectionWithProxy( isSharded: !!shardKey, }; - const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy"); + const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy); return window .fetch(`${endpoint}/createCollection`, { @@ -718,19 +718,78 @@ export function getEndpoint(endpoint: string): string { return url; } -export function useMongoProxyEndpoint(api: string): boolean { - const activeMongoProxyEndpoints: string[] = [ - MongoProxyEndpoints.Local, - MongoProxyEndpoints.Mpac, - MongoProxyEndpoints.Prod, - MongoProxyEndpoints.Fairfax, - MongoProxyEndpoints.Mooncake, - ]; +export function useMongoProxyEndpoint(mongoProxyApi: string): boolean { + const mongoProxyEnvironmentMap: { [key: string]: string[] } = { + [MongoProxyApi.ResourceList]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + [MongoProxyApi.QueryDocuments]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + [MongoProxyApi.CreateDocument]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + [MongoProxyApi.ReadDocument]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + [MongoProxyApi.UpdateDocument]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + [MongoProxyApi.DeleteDocument]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + [MongoProxyApi.CreateCollectionWithProxy]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + [MongoProxyApi.LegacyMongoShell]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + [MongoProxyApi.BulkDelete]: [ + MongoProxyEndpoints.Local, + MongoProxyEndpoints.Mpac, + MongoProxyEndpoints.Prod, + MongoProxyEndpoints.Fairfax, + MongoProxyEndpoints.Mooncake, + ], + }; - return ( - configContext.NEW_MONGO_APIS?.includes(api) && - activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT) - ); + if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) { + return false; + } + + return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT); } export class ThrottlingError extends Error { diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 86fd92586..86a024263 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -53,7 +53,6 @@ export interface ConfigContext { NEW_BACKEND_APIS?: BackendApi[]; MONGO_BACKEND_ENDPOINT?: string; MONGO_PROXY_ENDPOINT: string; - NEW_MONGO_APIS?: string[]; CASSANDRA_PROXY_ENDPOINT: string; NEW_CASSANDRA_APIS?: string[]; PROXY_PATH?: string; @@ -76,6 +75,7 @@ let configContext: Readonly = { allowedParentFrameOrigins: [ `^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`, `^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`, + `^https:\\/\\/cdb-(ms|ff|mc)-prod-pbe\\.cosmos\\.azure\\.(com|us|cn)$`, `^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`, `^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`, `^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`, @@ -106,17 +106,6 @@ let configContext: Readonly = { BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, - NEW_MONGO_APIS: [ - "resourcelist", - "queryDocuments", - "createDocument", - "readDocument", - "updateDocument", - "deleteDocument", - "createCollectionWithProxy", - "legacyMongoShell", - "bulkdelete", - ], CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"], isTerminalEnabled: false, diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index c5445c2f9..8bcb35aa1 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -2115,7 +2115,7 @@ export const DocumentsTabComponent: React.FunctionComponent { ${"https://subdomain.portal.azure.com"} | ${false} ${"https://subdomain.portal.azure.us"} | ${false} ${"https://subdomain.portal.azure.cn"} | ${false} - ${"https://main.documentdb.ext.azure.com"} | ${false} - ${"https://main.documentdb.ext.azure.us"} | ${false} - ${"https://main.documentdb.ext.azure.cn"} | ${false} + ${"https://cdb-ms-prod-pbe.cosmos.azure.com"} | ${false} + ${"https://cdb-ff-prod-pbe.cosmos.azure.us"} | ${false} + ${"https://cdb-mc-prod-pbe.cosmos.azure.cn"} | ${false} ${"https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de"} | ${false} ${"https://main.documentdb.ext.microsoftazure.de"} | ${false} ${"https://random.domain"} | ${true} diff --git a/src/Utils/NetworkUtility.test.ts b/src/Utils/NetworkUtility.test.ts index ba2eb2c67..b27c8db50 100644 --- a/src/Utils/NetworkUtility.test.ts +++ b/src/Utils/NetworkUtility.test.ts @@ -2,12 +2,11 @@ import { MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants"; import { resetConfigContext, updateConfigContext } from "ConfigContext"; import { DatabaseAccount, IpRule } from "Contracts/DataModels"; import { updateUserContext } from "UserContext"; -import { MongoProxyOutboundIPs, PortalBackendIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils"; +import { MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils"; import { getNetworkSettingsWarningMessage } from "./NetworkUtility"; describe("NetworkUtility tests", () => { describe("getNetworkSettingsWarningMessage", () => { - const legacyBackendEndpoint: string = "https://main.documentdb.ext.azure.com"; const publicAccessMessagePart = "Please enable public access to proceed"; const accessMessagePart = "Please allow access from Azure Portal to proceed"; let warningMessageResult: string; @@ -48,25 +47,23 @@ describe("NetworkUtility tests", () => { }); it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, async () => { - const portalBackendOutboundIPsWithLegacyIPs: string[] = [ + const portalBackendOutboundIPs: string[] = [ ...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac], ...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod], - ...PortalBackendIPs["https://main.documentdb.ext.azure.com"], ]; updateUserContext({ databaseAccount: { kind: "MongoDB", properties: { - ipRules: portalBackendOutboundIPsWithLegacyIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule), + ipRules: portalBackendOutboundIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule), publicNetworkAccess: "Enabled", }, } as DatabaseAccount, }); updateConfigContext({ - BACKEND_ENDPOINT: legacyBackendEndpoint, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac, }); @@ -90,7 +87,6 @@ describe("NetworkUtility tests", () => { }); updateConfigContext({ - BACKEND_ENDPOINT: legacyBackendEndpoint, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac, }); diff --git a/src/Utils/NetworkUtility.ts b/src/Utils/NetworkUtility.ts index 40f663624..8c3b02e20 100644 --- a/src/Utils/NetworkUtility.ts +++ b/src/Utils/NetworkUtility.ts @@ -2,12 +2,7 @@ import { CassandraProxyEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } import { configContext } from "ConfigContext"; import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; import { userContext } from "UserContext"; -import { - CassandraProxyOutboundIPs, - MongoProxyOutboundIPs, - PortalBackendIPs, - PortalBackendOutboundIPs, -} from "Utils/EndpointUtils"; +import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils"; export const getNetworkSettingsWarningMessage = async ( setStateFunc: (warningMessage: string) => void, @@ -61,7 +56,7 @@ export const getNetworkSettingsWarningMessage = async ( ...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod], ] : PortalBackendOutboundIPs[configContext.PORTAL_BACKEND_ENDPOINT]; - let portalIPs: string[] = [...portalBackendOutboundIPs, ...PortalBackendIPs[configContext.BACKEND_ENDPOINT]]; + let portalIPs: string[] = [...portalBackendOutboundIPs]; if (userContext.apiType === "Mongo") { const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes( diff --git a/webpack.config.js b/webpack.config.js index 043188275..c21baf3ef 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -301,7 +301,7 @@ module.exports = function (_env = {}, argv = {}) { }, proxy: { "/api": { - target: "https://main.documentdb.ext.azure.com", + target: "https://cdb-ms-mpac-pbe.cosmos.azure.com", changeOrigin: true, logLevel: "debug", bypass: (req, res) => { @@ -312,7 +312,7 @@ module.exports = function (_env = {}, argv = {}) { }, }, "/proxy": { - target: "https://main.documentdb.ext.azure.com", + target: "https://cdb-ms-mpac-pbe.cosmos.azure.com", changeOrigin: true, secure: false, logLevel: "debug", From eb0d7b71b365a48e9de8fc614e49995810c03119 Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:15:25 +0530 Subject: [PATCH 13/13] [accessibility-3100029]:[Screen Reader - Azure Cosmos DB - Add Table Row]: Descriptive Label is not provided for 'Value' edit fields under 'Add Table Row' pane. (#1970) Co-authored-by: Satyapriya Bai --- src/Common/EntityValue.tsx | 27 +++++++++++++++++---------- src/Common/TableEntity.tsx | 1 + 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Common/EntityValue.tsx b/src/Common/EntityValue.tsx index 4fb0403a9..2d8911c34 100644 --- a/src/Common/EntityValue.tsx +++ b/src/Common/EntityValue.tsx @@ -10,6 +10,7 @@ export interface TableEntityProps { isEntityValueDisable?: boolean; entityTimeValue: string; entityValueType: string; + entityProperty: string; onEntityValueChange: (event: React.FormEvent, newInput?: string) => void; onSelectDate: (date: Date | null | undefined) => void; onEntityTimeValueChange: (event: React.FormEvent, newInput?: string) => void; @@ -26,6 +27,7 @@ export const EntityValue: FunctionComponent = ({ onSelectDate, isEntityValueDisable, onEntityTimeValueChange, + entityProperty, }: TableEntityProps): JSX.Element => { if (isEntityTypeDate) { return ( @@ -51,15 +53,20 @@ export const EntityValue: FunctionComponent = ({ } return ( - + <> + + Edit Property {entityProperty} {attributeValueLabel} + + + ); }; diff --git a/src/Common/TableEntity.tsx b/src/Common/TableEntity.tsx index eece32ecb..8065b074d 100644 --- a/src/Common/TableEntity.tsx +++ b/src/Common/TableEntity.tsx @@ -135,6 +135,7 @@ export const TableEntity: FunctionComponent = ({ onEntityValueChange={onEntityValueChange} onSelectDate={onSelectDate} onEntityTimeValueChange={onEntityTimeValueChange} + entityProperty={entityProperty} /> {!isEntityValueDisable && (