diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 284405b54..dac26c32d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,8 +164,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] - shardTotal: [16] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + shardTotal: [20] steps: - uses: actions/checkout@v4 - name: Use Node.js 18.x @@ -192,24 +192,29 @@ jobs: NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken) echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN" echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV + NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken) + echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN" + echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken) echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN" echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken) echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN" echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV - CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken) - echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN" - echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV - MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken) - echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN" - echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV - MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken) - echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN" - echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV - MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken) - echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN" - echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV + # CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken) + # echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN" + # echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV + # MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken) + # echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN" + # echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV + # MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken) + # echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN" + # echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV + # MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken) + # echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN" + # echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV + - name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} + run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3 - name: Upload blob report to GitHub Actions Artifacts @@ -250,4 +255,4 @@ jobs: with: name: html-report--attempt-${{ github.run_attempt }} path: playwright-report - retention-days: 14 + retention-days: 14 \ No newline at end of file diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 6eed6ca0b..ece8c8dba 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -6,8 +6,8 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: schedule: - # Once every hour - - cron: "0 15 * * *" + # Once every two hours + - cron: "0 */2 * * *" permissions: id-token: write @@ -36,4 +36,4 @@ jobs: with: node-version: 18.x - run: npm ci - - run: node utils/cleanupDBs.js + - run: node utils/cleanupDBs.js \ No newline at end of file diff --git a/less/documentDB.less b/less/documentDB.less index 3950aad9c..83a2f1a2b 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -406,7 +406,11 @@ body { width: 440px; min-height: 565px; } - +.dataExplorerLoaderforcopyJobs{ + width: 100%; + min-height: 565px; + right: 0; +} .dataExplorerTabLoaderContainer { left: initial; top: initial; 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 { diff --git a/package-lock.json b/package-lock.json index 0663caa6d..b3f8d655e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,8 +116,8 @@ "tinykeys": "2.1.0", "underscore": "1.12.1", "utility-types": "3.10.0", - "web-vitals": "4.2.4", "uuid": "9.0.0", + "web-vitals": "4.2.4", "zustand": "3.5.0" }, "devDependencies": { diff --git a/playwright.config.ts b/playwright.config.ts index b1f6a622d..228a9373b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,8 +11,8 @@ export default defineConfig({ reporter: process.env.CI ? "blob" : "html", timeout: 10 * 60 * 1000, use: { - trace: "off", - video: "off", + trace: "retain-on-failure", + video: "retain-on-failure", screenshot: "on", testIdAttribute: "data-test", contextOptions: { diff --git a/src/Common/LoadingOverlay.tsx b/src/Common/LoadingOverlay.tsx index 320576533..96bf1943a 100644 --- a/src/Common/LoadingOverlay.tsx +++ b/src/Common/LoadingOverlay.tsx @@ -1,4 +1,5 @@ import { Overlay, Spinner, SpinnerSize } from "@fluentui/react"; +import { useThemeStore } from "hooks/useTheme"; import React from "react"; interface LoadingOverlayProps { @@ -7,15 +8,17 @@ interface LoadingOverlayProps { } const LoadingOverlay: React.FC = ({ isLoading, label }) => { + const isDarkMode = useThemeStore((state) => state.isDarkMode); if (!isLoading) { return null; } return ( = ({ isLoading, label }) => }, }} > - + ); }; diff --git a/src/Common/Pager/Pager.css b/src/Common/Pager/Pager.css index a29b6f50a..3379c2094 100644 --- a/src/Common/Pager/Pager.css +++ b/src/Common/Pager/Pager.css @@ -11,3 +11,14 @@ gap: 8px; align-items: center; } + +/* Override dark mode inherit for pagination icons */ +body.isDarkMode .pager-container .ms-Button .ms-Button-icon, +body.isDarkMode .pager-container .ms-Button i { + color: var(--colorBrandForeground1); +} + +body.isDarkMode .pager-container .ms-Button:disabled .ms-Button-icon, +body.isDarkMode .pager-container .ms-Button:disabled i { + color: var(--colorNeutralForegroundDisabled); +} \ No newline at end of file diff --git a/src/Common/Pager/index.tsx b/src/Common/Pager/index.tsx index 06ff7f2be..f85209c24 100644 --- a/src/Common/Pager/index.tsx +++ b/src/Common/Pager/index.tsx @@ -59,7 +59,7 @@ const Pager: React.FC = ({ return (
{showItemCount && ( - + Showing {startIndex + 1} - {endIndex} of {totalCount} items )} @@ -82,7 +82,7 @@ const Pager: React.FC = ({ disabled={disabled || currentPage === 1} styles={iconButtonStyles} /> - + Page {currentPage} of {totalPages}
=> { 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; }; diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx index a1885e062..9bb7fe3ea 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx @@ -1,5 +1,6 @@ import "@testing-library/jest-dom"; import Explorer from "Explorer/Explorer"; +import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers"; import * as Logger from "../../../Common/Logger"; import { useSidePanel } from "../../../hooks/useSidePanel"; import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs"; @@ -30,6 +31,7 @@ jest.mock("../../../Common/Logger"); jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs"); jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState"); jest.mock("../CopyJobUtils"); +jest.mock("../../../Common/dataAccess/dataTransfers"); describe("CopyJobActions", () => { beforeEach(() => { @@ -154,33 +156,31 @@ describe("CopyJobActions", () => { }); it("should fetch and format copy jobs successfully", async () => { - const mockResponse = { - value: [ - { - properties: { - jobName: "job-1", - status: "InProgress", - lastUpdatedUtcTime: "2025-01-01T10:00:00Z", - processedCount: 50, - totalCount: 100, - mode: "online", - duration: "01:30:45", - source: { - component: "CosmosDBSql", - databaseName: "source-db", - containerName: "source-container", - }, - destination: { - component: "CosmosDBSql", - databaseName: "target-db", - containerName: "target-container", - }, + const mockResponse = [ + { + properties: { + jobName: "job-1", + status: "InProgress", + lastUpdatedUtcTime: "2025-01-01T10:00:00Z", + processedCount: 50, + totalCount: 100, + mode: "online", + duration: "01:30:45", + source: { + component: "CosmosDBSql", + databaseName: "source-db", + containerName: "source-container", + }, + destination: { + component: "CosmosDBSql", + databaseName: "target-db", + containerName: "target-container", }, }, - ], - }; + }, + ]; - (dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse); + (getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse); (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({ formattedDateTime: "1/1/2025, 10:00:00 AM", timestamp: 1704106800000, @@ -201,38 +201,36 @@ describe("CopyJobActions", () => { }); it("should filter jobs by CosmosDBSql component", async () => { - const mockResponse = { - value: [ - { - properties: { - jobName: "sql-job", - status: "Completed", - lastUpdatedUtcTime: "2025-01-01T10:00:00Z", - processedCount: 100, - totalCount: 100, - mode: "offline", - duration: "02:00:00", - source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, - destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, - }, + const mockResponse = [ + { + properties: { + jobName: "sql-job", + status: "Completed", + lastUpdatedUtcTime: "2025-01-01T10:00:00Z", + processedCount: 100, + totalCount: 100, + mode: "offline", + duration: "02:00:00", + source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, + destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, }, - { - properties: { - jobName: "other-job", - status: "Completed", - lastUpdatedUtcTime: "2025-01-01T11:00:00Z", - processedCount: 100, - totalCount: 100, - mode: "offline", - duration: "01:00:00", - source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" }, - destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, - }, + }, + { + properties: { + jobName: "other-job", + status: "Completed", + lastUpdatedUtcTime: "2025-01-01T11:00:00Z", + processedCount: 100, + totalCount: 100, + mode: "offline", + duration: "01:00:00", + source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" }, + destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, }, - ], - }; + }, + ]; - (dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse); + (getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse); (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({ formattedDateTime: "1/1/2025, 10:00:00 AM", timestamp: 1704106800000, @@ -247,38 +245,36 @@ describe("CopyJobActions", () => { }); it("should sort jobs by last updated time (newest first)", async () => { - const mockResponse = { - value: [ - { - properties: { - jobName: "older-job", - status: "Completed", - lastUpdatedUtcTime: "2025-01-01T10:00:00Z", - processedCount: 100, - totalCount: 100, - mode: "offline", - duration: "01:00:00", - source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, - destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, - }, + const mockResponse = [ + { + properties: { + jobName: "older-job", + status: "Completed", + lastUpdatedUtcTime: "2025-01-01T10:00:00Z", + processedCount: 100, + totalCount: 100, + mode: "offline", + duration: "01:00:00", + source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, + destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, }, - { - properties: { - jobName: "newer-job", - status: "InProgress", - lastUpdatedUtcTime: "2025-01-02T10:00:00Z", - processedCount: 50, - totalCount: 100, - mode: "online", - duration: "00:30:00", - source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" }, - destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" }, - }, + }, + { + properties: { + jobName: "newer-job", + status: "InProgress", + lastUpdatedUtcTime: "2025-01-02T10:00:00Z", + processedCount: 50, + totalCount: 100, + mode: "online", + duration: "00:30:00", + source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" }, + destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" }, }, - ], - }; + }, + ]; - (dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse); + (getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse); (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({ formattedDateTime: "1/1/2025, 10:00:00 AM", timestamp: 1704106800000, @@ -293,25 +289,23 @@ describe("CopyJobActions", () => { }); it("should calculate completion percentage correctly", async () => { - const mockResponse = { - value: [ - { - properties: { - jobName: "job-1", - status: "InProgress", - lastUpdatedUtcTime: "2025-01-01T10:00:00Z", - processedCount: 75, - totalCount: 100, - mode: "online", - duration: "01:00:00", - source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, - destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, - }, + const mockResponse = [ + { + properties: { + jobName: "job-1", + status: "InProgress", + lastUpdatedUtcTime: "2025-01-01T10:00:00Z", + processedCount: 75, + totalCount: 100, + mode: "online", + duration: "01:00:00", + source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, + destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, }, - ], - }; + }, + ]; - (dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse); + (getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse); (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({ formattedDateTime: "1/1/2025, 10:00:00 AM", timestamp: 1704106800000, @@ -325,25 +319,23 @@ describe("CopyJobActions", () => { }); it("should handle zero total count gracefully", async () => { - const mockResponse = { - value: [ - { - properties: { - jobName: "job-1", - status: "Pending", - lastUpdatedUtcTime: "2025-01-01T10:00:00Z", - processedCount: 0, - totalCount: 0, - mode: "online", - duration: "00:00:00", - source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, - destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, - }, + const mockResponse = [ + { + properties: { + jobName: "job-1", + status: "Pending", + lastUpdatedUtcTime: "2025-01-01T10:00:00Z", + processedCount: 0, + totalCount: 0, + mode: "online", + duration: "00:00:00", + source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, + destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, }, - ], - }; + }, + ]; - (dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse); + (getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse); (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({ formattedDateTime: "1/1/2025, 10:00:00 AM", timestamp: 1704106800000, @@ -361,26 +353,24 @@ describe("CopyJobActions", () => { message: "Error message line 1\r\n\r\nError message line 2", code: "ErrorCode123", }; - const mockResponse = { - value: [ - { - properties: { - jobName: "failed-job", - status: "Failed", - lastUpdatedUtcTime: "2025-01-01T10:00:00Z", - processedCount: 50, - totalCount: 100, - mode: "offline", - duration: "00:30:00", - source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, - destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, - error: mockError, - }, + const mockResponse = [ + { + properties: { + jobName: "failed-job", + status: "Failed", + lastUpdatedUtcTime: "2025-01-01T10:00:00Z", + processedCount: 50, + totalCount: 100, + mode: "offline", + duration: "00:30:00", + source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" }, + destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, + error: mockError, }, - ], - }; + }, + ]; - (dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse); + (getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse); (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({ formattedDateTime: "1/1/2025, 10:00:00 AM", timestamp: 1704106800000, @@ -408,7 +398,7 @@ describe("CopyJobActions", () => { }; (global as any).AbortController = jest.fn(() => mockAbortController); - (dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ value: [] }); + (getDataTransferJobs as jest.Mock).mockResolvedValue([]); getCopyJobs(); expect(mockAbortController.abort).not.toHaveBeenCalled(); @@ -418,9 +408,7 @@ describe("CopyJobActions", () => { }); it("should throw error for invalid response format", async () => { - (dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ - value: "not-an-array", - }); + (getDataTransferJobs as jest.Mock).mockResolvedValue("not-an-array"); await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs."); }); @@ -430,7 +418,7 @@ describe("CopyJobActions", () => { message: "Aborted", content: JSON.stringify({ message: "signal is aborted without reason" }), }; - (dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError); + (getDataTransferJobs as jest.Mock).mockRejectedValue(abortError); await expect(getCopyJobs()).rejects.toMatchObject({ message: expect.stringContaining("Previous copy job request was cancelled."), @@ -439,7 +427,7 @@ describe("CopyJobActions", () => { it("should handle generic errors", async () => { const genericError = new Error("Network error"); - (dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(genericError); + (getDataTransferJobs as jest.Mock).mockRejectedValue(genericError); await expect(getCopyJobs()).rejects.toThrow("Network error"); }); diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx index e2e6b6fc7..821f87bc9 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx @@ -1,13 +1,13 @@ import Explorer from "Explorer/Explorer"; import React from "react"; import { userContext } from "UserContext"; +import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers"; import { logError } from "../../../Common/Logger"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { cancel, complete, create, - listByDatabaseAccount, pause, resume, } from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs"; @@ -63,14 +63,8 @@ export const getCopyJobs = async (): Promise => { const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId( userContext.databaseAccount?.id || "", ); - const response = await listByDatabaseAccount( - subscriptionId, - resourceGroup, - accountName, - copyJobsAbortController.signal, - ); + const jobs = await getDataTransferJobs(subscriptionId, resourceGroup, accountName, copyJobsAbortController.signal); - const jobs = response.value || []; if (!Array.isArray(jobs)) { throw new Error("Invalid migration job status response: Expected an array of jobs."); } diff --git a/src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.test.tsx b/src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.test.tsx index 314fecc37..e0d2b0198 100644 --- a/src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.test.tsx +++ b/src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.test.tsx @@ -39,7 +39,7 @@ describe("CopyJobCommandBar", () => { render(); - expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer); + expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false); expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1); }); @@ -163,7 +163,7 @@ describe("CopyJobCommandBar", () => { render(); - expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer); + expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false); expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps); }); @@ -175,11 +175,11 @@ describe("CopyJobCommandBar", () => { mockConvertButton.mockReturnValue([]); const { rerender } = render(); - expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1); + expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1, false); rerender(); - expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2); + expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2, false); expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2); }); }); diff --git a/src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.tsx b/src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.tsx index 92b1107c9..9cd2f5002 100644 --- a/src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.tsx +++ b/src/Explorer/ContainerCopy/CommandBar/CopyJobCommandBar.tsx @@ -1,24 +1,28 @@ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import React from "react"; -import { StyleConstants } from "../../../Common/StyleConstants"; +import { useThemeStore } from "../../../hooks/useTheme"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil"; +import { getThemeTokens } from "../../Theme/ThemeUtil"; import { ContainerCopyProps } from "../Types/CopyJobTypes"; import { getCommandBarButtons } from "./Utils"; -const backgroundColor = StyleConstants.BaseLight; -const rootStyle = { - root: { - backgroundColor: backgroundColor, - }, -}; - const CopyJobCommandBar: React.FC = ({ explorer }) => { - const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer); + const isDarkMode = useThemeStore((state) => state.isDarkMode); + const themeTokens = getThemeTokens(isDarkMode); + const backgroundColor = themeTokens.colorNeutralBackground1; + + const rootStyle = { + root: { + backgroundColor: backgroundColor, + }, + }; + + const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer, isDarkMode); const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor); return ( -
+
{ describe("getCommandBarButtons", () => { it("should return an array of command button props", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); expect(buttons).toBeDefined(); expect(Array.isArray(buttons)).toBe(true); @@ -58,7 +58,7 @@ describe("CommandBar Utils", () => { }); it("should include create copy job button", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); const createButton = buttons[0]; expect(createButton).toBeDefined(); @@ -70,7 +70,7 @@ describe("CommandBar Utils", () => { }); it("should include refresh button", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); const refreshButton = buttons[1]; expect(refreshButton).toBeDefined(); @@ -80,11 +80,11 @@ describe("CommandBar Utils", () => { }); it("should include feedback button when platform is Portal", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); - expect(buttons.length).toBe(3); + expect(buttons.length).toBe(4); - const feedbackButton = buttons[2]; + const feedbackButton = buttons[3]; expect(feedbackButton).toBeDefined(); expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs"); expect(feedbackButton.tooltipText).toBe("Feedback"); @@ -105,13 +105,13 @@ describe("CommandBar Utils", () => { })); const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils"); - const buttons = getCommandBarButtonsEmulator(mockExplorer); + const buttons = getCommandBarButtonsEmulator(mockExplorer, false); - expect(buttons.length).toBe(2); + expect(buttons.length).toBe(3); }); it("should call openCreateCopyJobPanel when create button is clicked", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); const createButton = buttons[0]; createButton.onCommandClick({} as React.SyntheticEvent); @@ -121,7 +121,7 @@ describe("CommandBar Utils", () => { }); it("should call refreshJobList when refresh button is clicked", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); const refreshButton = buttons[1]; refreshButton.onCommandClick({} as React.SyntheticEvent); @@ -130,8 +130,8 @@ describe("CommandBar Utils", () => { }); it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => { - const buttons = getCommandBarButtons(mockExplorer); - const feedbackButton = buttons[2]; + const buttons = getCommandBarButtons(mockExplorer, false); + const feedbackButton = buttons[3]; feedbackButton.onCommandClick({} as React.SyntheticEvent); @@ -139,7 +139,7 @@ describe("CommandBar Utils", () => { }); it("should return buttons with correct icon sources", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); expect(buttons[0].iconSrc).toBeDefined(); expect(buttons[0].iconAlt).toBe("Create Copy Job"); @@ -148,7 +148,10 @@ describe("CommandBar Utils", () => { expect(buttons[1].iconAlt).toBe("Refresh"); expect(buttons[2].iconSrc).toBeDefined(); - expect(buttons[2].iconAlt).toBe("Feedback"); + expect(buttons[2].iconAlt).toBe("Dark Theme"); + + expect(buttons[3].iconSrc).toBeDefined(); + expect(buttons[3].iconAlt).toBe("Feedback"); }); it("should handle null MonitorCopyJobsRefState ref gracefully", () => { @@ -157,14 +160,14 @@ describe("CommandBar Utils", () => { return selector(state); }); - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); const refreshButton = buttons[1]; expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow(); }); it("should set hasPopup to false for all buttons", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons.forEach((button) => { expect(button.hasPopup).toBe(false); @@ -172,7 +175,7 @@ describe("CommandBar Utils", () => { }); it("should set commandButtonLabel to undefined for all buttons", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons.forEach((button) => { expect(button.commandButtonLabel).toBeUndefined(); @@ -180,7 +183,7 @@ describe("CommandBar Utils", () => { }); it("should respect disabled state when provided", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons.forEach((button) => { expect(button.disabled).toBe(false); @@ -188,7 +191,7 @@ describe("CommandBar Utils", () => { }); it("should return CommandButtonComponentProps with all required properties", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons.forEach((button: CommandButtonComponentProps) => { expect(button).toHaveProperty("iconSrc"); @@ -202,18 +205,19 @@ describe("CommandBar Utils", () => { }); }); - it("should maintain button order: create, refresh, feedback", () => { - const buttons = getCommandBarButtons(mockExplorer); + it("should maintain button order: create, refresh, themeToggle, feedback", () => { + const buttons = getCommandBarButtons(mockExplorer, false); expect(buttons[0].tooltipText).toBe("Create Copy Job"); expect(buttons[1].tooltipText).toBe("Refresh"); - expect(buttons[2].tooltipText).toBe("Feedback"); + expect(buttons[2].tooltipText).toBe("Dark Theme"); + expect(buttons[3].tooltipText).toBe("Feedback"); }); }); describe("Button click handlers", () => { it("should execute click handlers without errors", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons.forEach((button) => { expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow(); @@ -221,7 +225,7 @@ describe("CommandBar Utils", () => { }); it("should call correct action for each button", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons[0].onCommandClick({} as React.SyntheticEvent); expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer); @@ -229,14 +233,14 @@ describe("CommandBar Utils", () => { buttons[1].onCommandClick({} as React.SyntheticEvent); expect(mockRefreshJobList).toHaveBeenCalled(); - buttons[2].onCommandClick({} as React.SyntheticEvent); + buttons[3].onCommandClick({} as React.SyntheticEvent); expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled(); }); }); describe("Accessibility", () => { it("should have aria labels for all buttons", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons.forEach((button) => { expect(button.ariaLabel).toBeDefined(); @@ -246,7 +250,7 @@ describe("CommandBar Utils", () => { }); it("should have tooltip text for all buttons", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons.forEach((button) => { expect(button.tooltipText).toBeDefined(); @@ -256,7 +260,7 @@ describe("CommandBar Utils", () => { }); it("should have icon alt text for all buttons", () => { - const buttons = getCommandBarButtons(mockExplorer); + const buttons = getCommandBarButtons(mockExplorer, false); buttons.forEach((button) => { expect(button.iconAlt).toBeDefined(); diff --git a/src/Explorer/ContainerCopy/CommandBar/Utils.ts b/src/Explorer/ContainerCopy/CommandBar/Utils.ts index 152c2dfbd..d00b3788d 100644 --- a/src/Explorer/ContainerCopy/CommandBar/Utils.ts +++ b/src/Explorer/ContainerCopy/CommandBar/Utils.ts @@ -1,7 +1,10 @@ import AddIcon from "../../../../images/Add.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg"; +import MoonIcon from "../../../../images/MoonIcon.svg"; import RefreshIcon from "../../../../images/refresh-cosmos.svg"; +import SunIcon from "../../../../images/SunIcon.svg"; import { configContext, Platform } from "../../../ConfigContext"; +import { useThemeStore } from "../../../hooks/useTheme"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; import * as Actions from "../Actions/CopyJobActions"; @@ -9,7 +12,7 @@ import ContainerCopyMessages from "../ContainerCopyMessages"; import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState"; import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes"; -function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] { +function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] { const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref); const buttons: CopyJobCommandBarBtnType[] = [ { @@ -26,7 +29,15 @@ function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] { ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel, onClick: () => monitorCopyJobsRef?.refreshJobList(), }, + { + key: "themeToggle", + iconSrc: isDarkMode ? SunIcon : MoonIcon, + label: isDarkMode ? "Light Theme" : "Dark Theme", + ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme", + onClick: () => useThemeStore.getState().toggleTheme(), + }, ]; + if (configContext.platform === Platform.Portal) { buttons.push({ key: "feedback", @@ -54,6 +65,6 @@ function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProp }; } -export function getCommandBarButtons(explorer: Explorer): CommandButtonComponentProps[] { - return getCopyJobBtns(explorer).map(btnMapper); +export function getCommandBarButtons(explorer: Explorer, isDarkMode: boolean): CommandButtonComponentProps[] { + return getCopyJobBtns(explorer, isDarkMode).map(btnMapper); } diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx index 1cff2c213..86c59611c 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx @@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle"; const managedIdentityTooltip = ( {ContainerCopyMessages.addManagedIdentity.tooltip.content}   - + {ContainerCopyMessages.addManagedIdentity.tooltip.hrefText} @@ -26,7 +31,7 @@ const AddManagedIdentity: React.FC = () => { return ( - + {ContainerCopyMessages.addManagedIdentity.description}  {ContainerCopyMessages.addManagedIdentity.descriptionHrefText} @@ -35,6 +40,7 @@ const AddManagedIdentity: React.FC = () => { {ContainerCopyMessages.readPermissionAssigned.tooltip.content}   - + {ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText} @@ -65,6 +70,7 @@ const AddReadPermissionToDefaultIdentity: React.FC = ({ 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,11 +44,12 @@ const PermissionGroup: React.FC = ({ title, description, return ( = ({ title, description, }} > - + {title} {description && ( - + {description} )} @@ -99,8 +100,12 @@ const AssignPermissions = () => { }, []); return ( - - + + {isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online ? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription( copyJobState?.source?.account?.name || "", diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx index 69e12e72e..92c752171 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx @@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle"; const managedIdentityTooltip = ( {ContainerCopyMessages.defaultManagedIdentity.tooltip.content}   - + {ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText} @@ -31,6 +36,7 @@ const DefaultManagedIdentity: React.FC = () => {
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}   - + {ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText} @@ -127,6 +132,7 @@ const PointInTimeRestore: React.FC = () => { {showRefreshButton ? ( { /> ) : ( A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.   @@ -67,6 +67,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] = class="ms-Toggle-background pill-117" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle1" role="switch" type="button" @@ -92,7 +93,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = ` class="ms-Stack addManagedIdentityContainer css-109" > A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.   @@ -154,6 +155,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = ` class="ms-Toggle-background pill-121" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle11" role="switch" type="button" @@ -173,10 +175,12 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
Enable system assigned managed identity Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button. @@ -261,7 +265,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi class="ms-Stack addManagedIdentityContainer css-109" > A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.   @@ -323,6 +327,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi class="ms-Toggle-background pill-121" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle3" role="switch" type="button" @@ -342,16 +347,17 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
Enable system assigned managed identity Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button. diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AddReadPermissionToDefaultIdentity.test.tsx.snap b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AddReadPermissionToDefaultIdentity.test.tsx.snap index e461c2058..e000b1285 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AddReadPermissionToDefaultIdentity.test.tsx.snap +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AddReadPermissionToDefaultIdentity.test.tsx.snap @@ -41,6 +41,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m class="ms-Toggle-background pill-115" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle17" role="switch" type="button" @@ -103,6 +104,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m class="ms-Toggle-background pill-115" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle16" role="switch" type="button" @@ -165,6 +167,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co class="ms-Toggle-background pill-115" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle3" role="switch" type="button" @@ -227,6 +230,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co class="ms-Toggle-background pill-119" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle1" role="switch" type="button" @@ -314,6 +318,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co class="ms-Toggle-background pill-115" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle0" role="switch" type="button" @@ -376,6 +381,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co class="ms-Toggle-background pill-115" data-is-focusable="true" data-ktp-target="true" + data-test="btn-toggle" id="Toggle2" role="switch" type="button" diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AssignPermissions.test.tsx.snap b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AssignPermissions.test.tsx.snap index a6d76c3f3..302ea4bf7 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AssignPermissions.test.tsx.snap +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AssignPermissions.test.tsx.snap @@ -4,6 +4,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
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