From be89c634f30f2e63eebf7469ccad5657d44e4519 Mon Sep 17 00:00:00 2001 From: BChoudhury-ms Date: Mon, 29 Dec 2025 15:08:54 +0530 Subject: [PATCH 1/6] Add E2E tests for partition key change workflow (#2293) --- .../PartitionKeyComponent.tsx | 4 +- .../PartitionKeyComponent.test.tsx.snap | 4 + .../ChangePartitionKeyPane.tsx | 17 +++- test/fx.ts | 9 ++ test/sql/query.spec.ts | 2 +- .../changePartitionKey.spec.ts | 98 +++++++++++++++++++ test/sql/scaleAndSettings/scale.spec.ts | 2 +- test/sql/scaleAndSettings/settings.spec.ts | 2 +- test/testData.ts | 16 ++- 9 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 test/sql/scaleAndSettings/changePartitionKey.spec.ts diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx index a58bf50cd..336a0d972 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx @@ -187,7 +187,7 @@ export const PartitionKeyComponent: React.FC = ({ Current {partitionKeyName.toLowerCase()} Partitioning - + {partitionKeyValue} {isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} @@ -199,6 +199,7 @@ export const PartitionKeyComponent: React.FC = ({ {!isReadOnly && ( <> = ({ {configContext.platform !== Platform.Emulator && ( = ({ {createNewContainer ? ( - + All configurations except for unique keys will be copied from the source container @@ -230,6 +230,7 @@ export const ChangePartitionKeyPane: React.FC = ({ = ({ = ({ type="text" id="addCollection-partitionKeyValue" key={`addCollection-partitionKeyValue_${index}`} + data-test={`new-container-sub-partition-key-input-${index}`} aria-required required size={40} @@ -327,6 +330,8 @@ export const ChangePartitionKeyPane: React.FC = ({ }} /> { @@ -339,6 +344,7 @@ export const ChangePartitionKeyPane: React.FC = ({ })} = Constants.BackendDefaults.maxNumMultiHashPartition} onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])} @@ -346,7 +352,11 @@ export const ChangePartitionKeyPane: React.FC = ({ Add hierarchical partition key {subPartitionKeys.length > 0 && ( - + This feature allows you to partition your data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "} @@ -359,7 +369,7 @@ export const ChangePartitionKeyPane: React.FC = ({ ) : ( - + @@ -390,6 +400,7 @@ export const ChangePartitionKeyPane: React.FC = ({ }} defaultSelectedKey={targetCollectionId} responsiveMode={999} + ariaLabel="Existing Containers" /> )} diff --git a/test/fx.ts b/test/fx.ts index 56e571635..3731f6e1e 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -470,6 +470,15 @@ export class DataExplorer { return this.frame.getByTestId("notification-console/header-status"); } + async getDropdownItemByName(name: string, ariaLabel?: string): Promise { + const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items"); + if (ariaLabel) { + expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel); + } + const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']"); + return containerDropdownItems.filter({ hasText: name }); + } + /** Waits for the Data Explorer app to load */ static async waitForExplorer(page: Page) { const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); diff --git a/test/sql/query.spec.ts b/test/sql/query.spec.ts index f574cd8fb..6368c4327 100644 --- a/test/sql/query.spec.ts +++ b/test/sql/query.spec.ts @@ -9,7 +9,7 @@ let queryTab: QueryTab = null!; let queryEditor: Editor = null!; test.beforeAll("Create Test Database", async () => { - context = await createTestSQLContainer(true); + context = await createTestSQLContainer({ includeTestData: true }); }); test.beforeEach("Open new query tab", async ({ page }) => { diff --git a/test/sql/scaleAndSettings/changePartitionKey.spec.ts b/test/sql/scaleAndSettings/changePartitionKey.spec.ts new file mode 100644 index 000000000..da9b422ef --- /dev/null +++ b/test/sql/scaleAndSettings/changePartitionKey.spec.ts @@ -0,0 +1,98 @@ +import { expect, Page, test } from "@playwright/test"; +import { DataExplorer, TestAccount } from "../../fx"; +import { createTestSQLContainer, TestContainerContext } from "../../testData"; + +test.describe("Change Partition Key", () => { + let pageInstance: Page; + let context: TestContainerContext = null!; + let explorer: DataExplorer = null!; + const newPartitionKeyPath = "/newPartitionKey"; + const newContainerId = "testcontainer_1"; + + test.beforeAll("Create Test Database", async () => { + context = await createTestSQLContainer(); + }); + + test.beforeEach("Open container settings", async ({ page }) => { + pageInstance = page; + explorer = await DataExplorer.open(page, TestAccount.SQL); + + // Click Scale & Settings and open Partition Key tab + await explorer.openScaleAndSettings(context); + const PartitionKeyTab = explorer.frame.getByTestId("settings-tab-header/PartitionKeyTab"); + await PartitionKeyTab.click(); + }); + + test.afterAll("Delete Test Database", async () => { + await context?.dispose(); + }); + + test("Change partition key path", async () => { + await expect(explorer.frame.getByText("/partitionKey")).toBeVisible(); + await expect(explorer.frame.getByText("Change partition key")).toBeVisible(); + await expect(explorer.frame.getByText(/To safeguard the integrity of/)).toBeVisible(); + await expect(explorer.frame.getByText(/To change the partition key/)).toBeVisible(); + + const changePartitionKeyButton = explorer.frame.getByTestId("change-partition-key-button"); + expect(changePartitionKeyButton).toBeVisible(); + await changePartitionKeyButton.click(); + + // Fill out new partition key form in the panel + const changePkPanel = explorer.frame.getByTestId(`Panel:Change partition key`); + await expect(changePkPanel.getByText(context.database.id)).toBeVisible(); + await expect(explorer.frame.getByRole("heading", { name: "Change partition key" })).toBeVisible(); + await expect(explorer.frame.getByText(/When changing a container/)).toBeVisible(); + + // Try to switch to new container + await expect(changePkPanel.getByText("New container")).toBeVisible(); + await expect(changePkPanel.getByText("Existing container")).toBeVisible(); + await expect(changePkPanel.getByTestId("new-container-id-input")).toBeVisible(); + + changePkPanel.getByTestId("new-container-id-input").fill(newContainerId); + await expect(changePkPanel.getByTestId("new-container-partition-key-input")).toBeVisible(); + changePkPanel.getByTestId("new-container-partition-key-input").fill(newPartitionKeyPath); + + await expect(changePkPanel.getByTestId("add-sub-partition-key-button")).toBeVisible(); + changePkPanel.getByTestId("add-sub-partition-key-button").click(); + await expect(changePkPanel.getByTestId("new-container-sub-partition-key-input-0")).toBeVisible(); + await expect(changePkPanel.getByTestId("remove-sub-partition-key-button-0")).toBeVisible(); + await expect(changePkPanel.getByTestId("hierarchical-partitioning-info-text")).toBeVisible(); + changePkPanel.getByTestId("new-container-sub-partition-key-input-0").fill("/customerId"); + + await changePkPanel.getByTestId("Panel/OkButton").click(); + + await pageInstance.waitForLoadState("networkidle"); + await expect(changePkPanel).not.toBeVisible({ timeout: 60 * 1000 }); + + // Verify partition key change job + const jobText = explorer.frame.getByText(/Partition key change job/); + await expect(jobText).toBeVisible(); + await expect(explorer.frame.locator(".ms-ProgressIndicator-itemName")).toContainText("Portal_testcontainer_1"); + + const jobRow = explorer.frame.locator(".ms-ProgressIndicator-itemDescription"); + await expect(jobRow.getByText("Completed")).toBeVisible({ timeout: 30 * 1000 }); + + const newContainerNode = await explorer.waitForContainerNode(context.database.id, newContainerId); + expect(newContainerNode).not.toBeNull(); + + // Now try to switch to existing container + await changePartitionKeyButton.click(); + await changePkPanel.getByText("Existing container").click(); + await changePkPanel.getByLabel("Use existing container").check(); + await changePkPanel.getByText("Choose an existing container").click(); + + const containerDropdownItem = await explorer.getDropdownItemByName(newContainerId, "Existing Containers"); + await containerDropdownItem.click(); + + await changePkPanel.getByTestId("Panel/OkButton").click(); + await explorer.frame.getByRole("button", { name: "Cancel" }).click(); + + // Dismiss overlay if it appears + const overlayFrame = explorer.frame.locator("#webpack-dev-server-client-overlay").first(); + if (await overlayFrame.count()) { + await overlayFrame.contentFrame().getByLabel("Dismiss").click(); + } + const cancelledJobRow = explorer.frame.getByTestId("Tab:tab0"); + await expect(cancelledJobRow.getByText("Cancelled")).toBeVisible({ timeout: 30 * 1000 }); + }); +}); diff --git a/test/sql/scaleAndSettings/scale.spec.ts b/test/sql/scaleAndSettings/scale.spec.ts index e3035a7dc..4937b1738 100644 --- a/test/sql/scaleAndSettings/scale.spec.ts +++ b/test/sql/scaleAndSettings/scale.spec.ts @@ -14,7 +14,7 @@ test.describe("Autoscale and Manual throughput", () => { let explorer: DataExplorer = null!; test.beforeAll("Create Test Database", async () => { - context = await createTestSQLContainer(true); + context = await createTestSQLContainer({ includeTestData: true }); }); test.beforeEach("Open container settings", async ({ page }) => { diff --git a/test/sql/scaleAndSettings/settings.spec.ts b/test/sql/scaleAndSettings/settings.spec.ts index 463cdacb7..894f5444b 100644 --- a/test/sql/scaleAndSettings/settings.spec.ts +++ b/test/sql/scaleAndSettings/settings.spec.ts @@ -7,7 +7,7 @@ test.describe("Settings under Scale & Settings", () => { let explorer: DataExplorer = null!; test.beforeAll("Create Test Database", async () => { - context = await createTestSQLContainer(true); + context = await createTestSQLContainer({ includeTestData: true }); }); test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => { diff --git a/test/testData.ts b/test/testData.ts index 94d38941f..9729a90b4 100644 --- a/test/testData.ts +++ b/test/testData.ts @@ -74,8 +74,18 @@ export class TestContainerContext { } } -export async function createTestSQLContainer(includeTestData?: boolean) { - const databaseId = generateUniqueName("db"); +type createTestSqlContainerConfig = { + includeTestData?: boolean; + partitionKey?: string; + databaseName?: string; +}; + +export async function createTestSQLContainer({ + includeTestData = false, + partitionKey = "/partitionKey", + databaseName = "", +}: createTestSqlContainerConfig = {}) { + const databaseId = databaseName ? databaseName : generateUniqueName("db"); const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique const credentials = getAzureCLICredentials(); const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials); @@ -104,7 +114,7 @@ export async function createTestSQLContainer(includeTestData?: boolean) { try { const { container } = await database.containers.createIfNotExists({ id: containerId, - partitionKey: "/partitionKey", + partitionKey, }); if (includeTestData) { const batchCount = TestData.length / 100; From 6167f94bc3c86002387e47c3e55264a7162f824e Mon Sep 17 00:00:00 2001 From: BChoudhury-ms Date: Mon, 29 Dec 2025 21:46:47 +0530 Subject: [PATCH 2/6] fix: restore SidePanel component for Container Copy feature (#2295) --- src/Main.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Main.tsx b/src/Main.tsx index ddb6a22fe..f30ff9902 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -125,7 +125,10 @@ const App = (): JSX.Element => {
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? ( - + <> + + + ) : ( )} From c8ebca6da42cbb444cbc724a2813a35436700c47 Mon Sep 17 00:00:00 2001 From: sakshigupta12feb Date: Mon, 5 Jan 2026 17:02:19 +0530 Subject: [PATCH 3/6] Fixed homepage UI for fabric (#2304) * Fixed homePage UI for fabric Co-authored-by: Sakshi Gupta --- less/documentDBFabric.less | 1 + 1 file changed, 1 insertion(+) diff --git a/less/documentDBFabric.less b/less/documentDBFabric.less index 7e3c15429..c1c0ec00c 100644 --- a/less/documentDBFabric.less +++ b/less/documentDBFabric.less @@ -218,6 +218,7 @@ a:focus { .tabPanesContainer { overflow: auto !important; + display: flex; } .tabs-container { From 258a6286e70cee53be5cb61a8c25608dc6c5e8a7 Mon Sep 17 00:00:00 2001 From: BChoudhury-ms Date: Tue, 6 Jan 2026 21:12:19 +0530 Subject: [PATCH 4/6] refactor(dataTransfers): replace fetch with armRequest for pagination (#2305) * refactor(dataTransfers): replace fetch with armRequest for pagination * added comment for next link url parsing --- src/Common/dataAccess/dataTransfers.ts | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Common/dataAccess/dataTransfers.ts b/src/Common/dataAccess/dataTransfers.ts index e639f9965..3e8829486 100644 --- a/src/Common/dataAccess/dataTransfers.ts +++ b/src/Common/dataAccess/dataTransfers.ts @@ -1,3 +1,4 @@ +import { configContext } from "ConfigContext"; import { ApiType, userContext } from "UserContext"; import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils"; import { @@ -14,9 +15,12 @@ import { DataTransferJobFeedResults, DataTransferJobGetResults, } from "Utils/arm/generatedClients/dataTransferService/types"; +import { armRequest } from "Utils/arm/request"; import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs"; import promiseRetry, { AbortError, FailedAttemptError } from "p-retry"; +export const DATA_TRANSFER_JOB_API_VERSION = "2025-05-01-preview"; + export interface DataTransferParams { jobName: string; apiType: ApiType; @@ -33,26 +37,34 @@ export const getDataTransferJobs = async ( subscriptionId: string, resourceGroup: string, accountName: string, + signal?: AbortSignal, ): Promise => { let dataTransferJobs: DataTransferJobGetResults[] = []; let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount( subscriptionId, resourceGroup, accountName, + signal, ); dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])]; while (dataTransferFeeds?.nextLink) { - const nextResponse = await window.fetch(dataTransferFeeds.nextLink, { - headers: { - Authorization: userContext.authorizationToken, - }, + /** + * The `nextLink` URL returned by the Cosmos DB SQL API pointed to an incorrect endpoint, causing timeouts. + * (i.e: https://cdbmgmtprodby.documents.azure.com:450/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.DocumentDB/databaseAccounts/{account}/sql/dataTransferJobs?$top=100&$skiptoken=...) + * We manipulate the URL by parsing it to extract the path and query parameters, + * then construct the correct URL for the Azure Resource Manager (ARM) API. + * This ensures that the request is made to the correct base URL (`configContext.ARM_ENDPOINT`), + * which is required for ARM operations. + */ + const parsedUrl = new URL(dataTransferFeeds.nextLink); + const nextUrlPath = parsedUrl.pathname + parsedUrl.search; + dataTransferFeeds = await armRequest({ + host: configContext.ARM_ENDPOINT, + path: nextUrlPath, + method: "GET", + apiVersion: DATA_TRANSFER_JOB_API_VERSION, }); - if (nextResponse.ok) { - dataTransferFeeds = await nextResponse.json(); - dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])]; - } else { - break; - } + dataTransferJobs.push(...(dataTransferFeeds?.value || [])); } return dataTransferJobs; }; From 53288dec6fe2bb46c499089f47ff9ef74d21e1be Mon Sep 17 00:00:00 2001 From: BChoudhury-ms Date: Tue, 6 Jan 2026 21:19:58 +0530 Subject: [PATCH 5/6] feat: add test identifiers (data-test) to Container Copy components (#2306) --- src/Common/LoadingOverlay.tsx | 1 + .../LoadingOverlay.test.tsx.snap | 4 ++ .../AssignPermissions/AddManagedIdentity.tsx | 1 + .../AddReadPermissionToDefaultIdentity.tsx | 1 + .../AssignPermissions/AssignPermissions.tsx | 13 ++++-- .../DefaultManagedIdentity.tsx | 1 + .../AssignPermissions/PointInTimeRestore.tsx | 2 + .../AddManagedIdentity.test.tsx.snap | 6 +++ ...dPermissionToDefaultIdentity.test.tsx.snap | 6 +++ .../AssignPermissions.test.tsx.snap | 42 +++++++++++++++++++ .../DefaultManagedIdentity.test.tsx.snap | 5 +++ .../PointInTimeRestore.test.tsx.snap | 5 +++ .../Screens/Components/NavigationControls.tsx | 18 ++++++-- .../Screens/Components/PopoverContainer.tsx | 1 + .../PopoverContainer.test.tsx.snap | 8 ++++ .../Screens/CreateCopyJobScreens.tsx | 1 + .../Screens/PreviewCopyJob/PreviewCopyJob.tsx | 8 ++-- .../PreviewCopyJob.test.tsx.snap | 34 +++++++++++++++ .../Components/AccountDropdown.test.tsx | 27 +++++++++--- .../Components/AccountDropdown.tsx | 13 ++++-- .../Components/MigrationTypeCheckbox.tsx | 2 +- .../MigrationTypeCheckbox.test.tsx.snap | 2 + .../SelectSourceAndTargetContainers.tsx | 8 +++- .../DatabaseContainerSection.test.tsx | 6 +++ .../components/DatabaseContainerSection.tsx | 3 ++ .../DatabaseContainerSection.test.tsx.snap | 8 ++++ .../Components/CopyJobActionMenu.tsx | 1 + .../Components/CopyJobsList.tsx | 1 + .../ContainerCopy/Types/CopyJobTypes.ts | 1 + 29 files changed, 206 insertions(+), 23 deletions(-) diff --git a/src/Common/LoadingOverlay.tsx b/src/Common/LoadingOverlay.tsx index 320576533..2cbf34213 100644 --- a/src/Common/LoadingOverlay.tsx +++ b/src/Common/LoadingOverlay.tsx @@ -13,6 +13,7 @@ const LoadingOverlay: React.FC = ({ isLoading, label }) => return (
= () => { = ({ id, title, Component, completed, disabled }) => ( - + {title} @@ -25,13 +25,13 @@ const PermissionSection: React.FC = ({ id, title, Compo height={completed ? 20 : 24} /> - + ); -const PermissionGroup: React.FC = ({ title, description, sections }) => { +const PermissionGroup: React.FC = ({ id, title, description, sections }) => { const [openItems, setOpenItems] = React.useState([]); useEffect(() => { @@ -44,6 +44,7 @@ const PermissionGroup: React.FC = ({ title, description, return ( { }, []); return ( - + {isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online ? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription( diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx index 69e12e72e..3eeb60bbf 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx @@ -31,6 +31,7 @@ const DefaultManagedIdentity: React.FC = () => {
{ {showRefreshButton ? ( { /> ) : (
Incomplete Component @@ -142,6 +147,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
Incomplete Component @@ -339,6 +350,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
Incomplete Component @@ -536,6 +553,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
Incomplete Component @@ -733,6 +756,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
@@ -301,6 +305,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot with refresh button