From 2f858ecf9b5544e4f25136320fc1e9b48c076f64 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Tue, 29 Apr 2025 17:50:20 +0200 Subject: [PATCH] Fabric native improvements: Settings pane, Partition Key settings tab, sample data and message contract (#2119) * Hide entire Accordion of options in Settings Pane * In PartitionKeyComponent hide "Change partition key" label when read-only. * Create sample data container with correct pkey * Add unit tests to PartitionKeyComponent * Fix format * fix unit test snapshot * Add Fabric message to open Settings to given tab id * Improve syntax on message contract * Remove "(preview)" in partition key tab title in Settings Tab --- src/Contracts/DataExplorerMessagesContract.ts | 9 +- src/Contracts/FabricMessageTypes.ts | 1 + .../PartitionKeyComponent.test.tsx | 41 + .../PartitionKeyComponent.tsx | 2 +- .../PartitionKeyComponent.test.tsx.snap | 196 ++++ .../Controls/Settings/SettingsUtils.tsx | 3 +- .../Panes/SettingsPane/SettingsPane.tsx | 838 +++++++++--------- src/Explorer/SplashScreen/SampleUtil.ts | 8 + 8 files changed, 677 insertions(+), 421 deletions(-) create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/PartitionKeyComponent.test.tsx.snap diff --git a/src/Contracts/DataExplorerMessagesContract.ts b/src/Contracts/DataExplorerMessagesContract.ts index a38940120..c017bffa8 100644 --- a/src/Contracts/DataExplorerMessagesContract.ts +++ b/src/Contracts/DataExplorerMessagesContract.ts @@ -18,10 +18,13 @@ export type DataExploreMessageV3 = | { type: FabricMessageTypes.GetAllResourceTokens; id: string; + } + | { + type: FabricMessageTypes.OpenSettings; + settingsId: string; }; - -export type GetCosmosTokenMessageOptions = { +export interface GetCosmosTokenMessageOptions { verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges"; resourceId: string; -}; +} diff --git a/src/Contracts/FabricMessageTypes.ts b/src/Contracts/FabricMessageTypes.ts index 1d4576391..02871ca47 100644 --- a/src/Contracts/FabricMessageTypes.ts +++ b/src/Contracts/FabricMessageTypes.ts @@ -6,6 +6,7 @@ export enum FabricMessageTypes { GetAllResourceTokens = "GetAllResourceTokens", GetAccessToken = "GetAccessToken", Ready = "Ready", + OpenSettings = "OpenSettings", } export interface AuthorizationToken { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx new file mode 100644 index 000000000..dfde4ec78 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx @@ -0,0 +1,41 @@ +import { shallow } from "enzyme"; +import { + PartitionKeyComponent, + PartitionKeyComponentProps, +} from "Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent"; +import Explorer from "Explorer/Explorer"; +import React from "react"; + +describe("PartitionKeyComponent", () => { + // Create a test setup function to get fresh instances for each test + const setupTest = () => { + // Create an instance of the mocked Explorer + const explorer = new Explorer(); + // Create minimal mock objects for database and collection + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockDatabase = {} as any as import("../../../../Contracts/ViewModels").Database; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockCollection = {} as any as import("../../../../Contracts/ViewModels").Collection; + + // Create props with the mocked Explorer instance + const props: PartitionKeyComponentProps = { + database: mockDatabase, + collection: mockCollection, + explorer, + }; + + return { explorer, props }; + }; + + it("renders default component and matches snapshot", () => { + const { props } = setupTest(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("renders read-only component and matches snapshot", () => { + const { props } = setupTest(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx index c6a1bd9d1..89810bcb6 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx @@ -161,7 +161,7 @@ export const PartitionKeyComponent: React.FC = ({ return ( - Change {partitionKeyName.toLowerCase()} + {!isReadOnly && Change {partitionKeyName.toLowerCase()}} Current {partitionKeyName.toLowerCase()} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/PartitionKeyComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/PartitionKeyComponent.test.tsx.snap new file mode 100644 index 000000000..95d87da3d --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/PartitionKeyComponent.test.tsx.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PartitionKeyComponent renders default component and matches snapshot 1`] = ` + + + + Change + partition key + + + + + Current + partition key + + + Partitioning + + + + + + Non-hierarchical + + + + + + To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the source container for the entire duration of the partition key change process. + + Learn more + + + + To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container. + + + +`; + +exports[`PartitionKeyComponent renders read-only component and matches snapshot 1`] = ` + + + + + + Current + partition key + + + Partitioning + + + + + + Non-hierarchical + + + + + +`; diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 448b59370..2617af6ac 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -1,6 +1,7 @@ import * as Constants from "../../../Common/Constants"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; +import { isFabricNative } from "../../../Platform/Fabric/FabricUtil"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; const zeroValue = 0; @@ -165,7 +166,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { case SettingsV2TabTypes.IndexingPolicyTab: return "Indexing Policy"; case SettingsV2TabTypes.PartitionKeyTab: - return "Partition Keys (preview)"; + return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)"; case SettingsV2TabTypes.ComputedPropertiesTab: return "Computed Properties"; case SettingsV2TabTypes.ContainerVectorPolicyTab: diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index a40f4da99..ca92b59ed 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -23,7 +23,7 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { useDatabases } from "Explorer/useDatabases"; -import { isFabric } from "Platform/Fabric/FabricUtil"; +import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil"; import { AppStateComponentNames, deleteAllStates, @@ -607,441 +607,447 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ return (
- - {shouldShowQueryPageOptions && ( - - -
Page Options
-
- -
-
- Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as - many query results per page. -
- -
-
- {isCustomPageOptionSelected() && ( -
-
- Query results per page{" "} - - Enter the number of query results that should be shown per page. - -
- - { - setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage); - }} - onDecrement={(newValue) => setCustomItemPerPage(parseInt(newValue) - 1 || customItemPerPage)} - onValidate={(newValue) => setCustomItemPerPage(parseInt(newValue) || customItemPerPage)} - min={1} - step={1} - className="textfontclr" - incrementButtonAriaLabel="Increase value by 1" - decrementButtonAriaLabel="Decrease value by 1" - /> -
- )} -
-
-
- )} - {showEnableEntraIdRbac && ( - - -
Enable Entra ID RBAC
-
- -
-
- Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID - RBAC. - - {" "} - Learn more{" "} - -
- -
-
-
- )} - {userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && ( - - -
Region Selection
-
- -
-
- Changes region the Cosmos Client uses to access account. -
-
- Select Region - - Changes the account endpoint used to perform client operations. - -
- option.key === selectedRegionalEndpoint)?.text - : regionOptions[0]?.text - } - onChange={handleOnSelectedRegionOptionChange} - options={regionOptions} - styles={{ root: { marginBottom: "10px" } }} - /> -
-
-
- )} - {userContext.apiType === "SQL" && !isEmulator && ( - <> - + {!isFabricNative() && ( + + {shouldShowQueryPageOptions && ( + -
Query Timeout
+
Page Options
- When a query reaches a specified time limit, a popup with an option to cancel the query will show - unless automatic cancellation has been enabled. -
- -
- {queryTimeoutEnabled && ( -
- - -
- )} -
-
- - - -
RU Limit
-
- -
-
- If a query exceeds a configured RU limit, the query will be aborted. -
- -
- {ruThresholdEnabled && ( -
- -
- )} -
-
- - - -
Default Query Results View
-
- -
-
- Select the default view to use when displaying query results. + Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as + many query results per page.
+
+
+ {isCustomPageOptionSelected() && ( +
+
+ Query results per page{" "} + + Enter the number of query results that should be shown per page. + +
+ + { + setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage); + }} + onDecrement={(newValue) => setCustomItemPerPage(parseInt(newValue) - 1 || customItemPerPage)} + onValidate={(newValue) => setCustomItemPerPage(parseInt(newValue) || customItemPerPage)} + min={1} + step={1} + className="textfontclr" + incrementButtonAriaLabel="Increase value by 1" + decrementButtonAriaLabel="Decrease value by 1" + /> +
+ )} +
+
+
+ )} + {showEnableEntraIdRbac && ( + + +
Enable Entra ID RBAC
+
+ +
+
+ Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra + ID RBAC. + + {" "} + Learn more{" "} + +
+
- - )} + )} + {userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && ( + + +
Region Selection
+
+ +
+
+ Changes region the Cosmos Client uses to access account. +
+
+ Select Region + + Changes the account endpoint used to perform client operations. + +
+ option.key === selectedRegionalEndpoint)?.text + : regionOptions[0]?.text + } + onChange={handleOnSelectedRegionOptionChange} + options={regionOptions} + styles={{ root: { marginBottom: "10px" } }} + /> +
+
+
+ )} + {userContext.apiType === "SQL" && !isEmulator && ( + <> + + +
Query Timeout
+
+ +
+
+ When a query reaches a specified time limit, a popup with an option to cancel the query will + show unless automatic cancellation has been enabled. +
+ +
+ {queryTimeoutEnabled && ( +
+ + +
+ )} +
+
- {showRetrySettings && ( - - -
Retry Settings
-
- -
-
- Retry policy associated with throttled requests during CosmosDB queries. + + +
RU Limit
+
+ +
+
+ If a query exceeds a configured RU limit, the query will be aborted. +
+ +
+ {ruThresholdEnabled && ( +
+ +
+ )} +
+
+ + + +
Default Query Results View
+
+ +
+
+ Select the default view to use when displaying query results. +
+ +
+
+
+ + )} + + {showRetrySettings && ( + + +
Retry Settings
+
+ +
+
+ Retry policy associated with throttled requests during CosmosDB queries. +
+
+ Max retry attempts + + Max number of retries to be performed for a request. Default value 9. + +
+ setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)} + onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)} + onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)} + styles={spinButtonStyles} + /> +
+ Fixed retry interval (ms) + + Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned + as part of the response. Default value is 0 milliseconds. + +
+ setRetryInterval(parseInt(newValue) + 1000 || retryInterval)} + onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)} + onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)} + styles={spinButtonStyles} + /> +
+ Max wait time (s) + + Max wait time in seconds to wait for a request while the retries are happening. Default value 30 + seconds. + +
+ + setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds) + } + onDecrement={(newValue) => + setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds) + } + onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)} + styles={spinButtonStyles} + />
-
- Max retry attempts - - Max number of retries to be performed for a request. Default value 9. - + + + )} + {!isEmulator && ( + + +
Enable container pagination
+
+ +
+
+ Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order. +
+ setContainerPaginationEnabled(!containerPaginationEnabled)} + label="Enable container pagination" + />
- setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)} - onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)} - onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)} - styles={spinButtonStyles} - /> -
- Fixed retry interval (ms) - - Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned - as part of the response. Default value is 0 milliseconds. - + + + )} + {shouldShowCrossPartitionOption && ( + + +
Enable cross-partition query
+
+ +
+
+ Send more than one request while executing a query. More than one request is necessary if the + query is not scoped to single partition key value. +
+ setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)} + label="Enable cross-partition query" + />
- setRetryInterval(parseInt(newValue) + 1000 || retryInterval)} - onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)} - onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)} - styles={spinButtonStyles} - /> -
- Max wait time (s) - - Max wait time in seconds to wait for a request while the retries are happening. Default value 30 - seconds. - + + + )} + {shouldShowParallelismOption && ( + + +
Max degree of parallelism
+
+ +
+
+ Gets or sets the number of concurrent operations run client side during parallel query execution. + A positive property value limits the number of concurrent operations to the set value. If it is + set to less than 0, the system automatically decides the number of concurrent operations to run. +
+ + setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism) + } + onDecrement={(newValue) => + setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism) + } + onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)} + ariaLabel="Max degree of parallelism" + label="Max degree of parallelism" + />
- setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)} - onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)} - onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)} - styles={spinButtonStyles} - /> -
-
-
- )} - {!isEmulator && ( - - -
Enable container pagination
-
- -
-
- Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order. + + + )} + {shouldShowPriorityLevelOption && ( + + +
Priority Level
+
+ +
+
+ Sets the priority level for data-plane requests from Data Explorer when using Priority-Based + Execution. If "None" is selected, Data Explorer will not specify priority level, and the + server-side default priority level will be used. +
+
- setContainerPaginationEnabled(!containerPaginationEnabled)} - label="Enable container pagination" - /> -
- - - )} - {shouldShowCrossPartitionOption && ( - - -
Enable cross-partition query
-
- -
-
- Send more than one request while executing a query. More than one request is necessary if the query - is not scoped to single partition key value. + + + )} + {shouldShowGraphAutoVizOption && ( + + +
Display Gremlin query results as: 
+
+ +
+
+ Select Graph to automatically visualize the query results as a Graph or JSON to display the + results as JSON. +
+
- setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)} - label="Enable cross-partition query" - /> -
- - - )} - {shouldShowParallelismOption && ( - - -
Max degree of parallelism
-
- -
-
- Gets or sets the number of concurrent operations run client side during parallel query execution. A - positive property value limits the number of concurrent operations to the set value. If it is set to - less than 0, the system automatically decides the number of concurrent operations to run. + + + )} + {shouldShowCopilotSampleDBOption && ( + + +
Enable sample database
+
+ +
+
+ This is a sample database and collection with synthetic product data you can use to explore using + 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. +
+
- - setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism) - } - onDecrement={(newValue) => - setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism) - } - onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)} - ariaLabel="Max degree of parallelism" - label="Max degree of parallelism" - /> -
- - - )} - {shouldShowPriorityLevelOption && ( - - -
Priority Level
-
- -
-
- Sets the priority level for data-plane requests from Data Explorer when using Priority-Based - Execution. If "None" is selected, Data Explorer will not specify priority level, and the - server-side default priority level will be used. -
- -
-
-
- )} - {shouldShowGraphAutoVizOption && ( - - -
Display Gremlin query results as: 
-
- -
-
- Select Graph to automatically visualize the query results as a Graph or JSON to display the results - as JSON. -
- -
-
-
- )} - {shouldShowCopilotSampleDBOption && ( - - -
Enable sample database
-
- -
-
- This is a sample database and collection with synthetic product data you can use to explore using - 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. -
- -
-
-
- )} - + + + )} + + )}
diff --git a/src/Explorer/SplashScreen/SampleUtil.ts b/src/Explorer/SplashScreen/SampleUtil.ts index 837227c8f..4072eb011 100644 --- a/src/Explorer/SplashScreen/SampleUtil.ts +++ b/src/Explorer/SplashScreen/SampleUtil.ts @@ -1,3 +1,4 @@ +import { BackendDefaults } from "Common/Constants"; import { createCollection } from "Common/dataAccess/createCollection"; import Explorer from "Explorer/Explorer"; import { useDatabases } from "Explorer/useDatabases"; @@ -35,6 +36,11 @@ export const createContainer = async ( collectionId: containerName, databaseId: databaseName, databaseLevelThroughput: false, + partitionKey: { + paths: [`/${SAMPLE_DATA_PARTITION_KEY}`], + kind: "Hash", + version: BackendDefaults.partitionKeyVersion, + }, }; await createCollection(createRequest); await explorer.refreshAllDatabases(); @@ -47,6 +53,8 @@ export const createContainer = async ( return newCollection; }; +const SAMPLE_DATA_PARTITION_KEY = "category"; // This pkey is specifically set for queryCopilotSampleData.json below + export const importData = async (collection: ViewModels.Collection): Promise => { // TODO: keep same chunk as ContainerSampleGenerator const dataFileContent = await import(