Compare commits

..

37 Commits

Author SHA1 Message Date
asier-isayas
28a7c03406 Merge branch 'master' into users/aisayas/playwright-3 2026-01-08 11:33:32 -05:00
BChoudhury-ms
38823ac86f Fix change partition key FTs (#2309) 2026-01-08 20:15:25 +05:30
Asier Isayas
6c32c7832a Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/playwright-3 2026-01-08 09:29:01 -05:00
Asier Isayas
48e8b58873 keep cleanup at 2 hours 2026-01-08 08:32:53 -05:00
Asier Isayas
ca7009a01e run cleanup every 3 hours 2026-01-08 07:25:39 -05:00
BChoudhury-ms
b71ea50972 Add test infrastructure and data-test attributes for Container Copy e2e tests (#2280)
* Add test infrastructure and data-test attributes for Container Copy e2e testing

* fix container copy FTs
2026-01-08 17:19:16 +05:30
sakshigupta12feb
e27cff0553 Copy jobs dark theme (#2308)
* added a dark theme toggle button on Copyjobs next to refresh button and covered full feature

* Fix formatting in Utils.test.ts

* updated infor icon , error icon and text on jobs details page

* rebased from master

* updated the conflicts

* updated the conflicts

* fixed the test suit

* fixed review comments

* test fix

---------

Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-01-08 13:27:57 +05:30
Asier Isayas
ac408eb255 fix lint 2026-01-07 18:50:38 -05:00
Asier Isayas
69d810c120 fix lint errors 2026-01-07 18:40:09 -05:00
Asier Isayas
b26ed59d2a fix tests 2026-01-07 18:35:28 -05:00
Asier Isayas
9e60122e2e fix upload items 2026-01-07 13:43:06 -05:00
Asier Isayas
b62036e03b Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/playwright-3 2026-01-07 12:49:02 -05:00
Asier Isayas
6785bf3cc8 Add user defined functions and trigger test 2026-01-07 11:18:14 -05:00
asier-isayas
4ac8cd8fe4 Fix playwright tests (#2285)
* Temporarily re-enable key based auth for Mongo and Cassandra tests.

* Increase number of shards for playwright tests.

* Another small bump to test shard count.

* click global new... button then collection in playwright tests

* get new table button

* create and delete container for every individual scale test

* for scale and settings, dont create sample data in container

* run scale tests serially

* refactor scale setup and tear down to be within each test

* record network traces

* record network calls on all retries

* when disposing of database during playwright test, refresh tree to remove deleted database

* refresh tree before  opening scale and settings

* When opening scale and settings, refresh databases

* reload all databases before loading offers

* increase time for change partition key request

* increase time for change partition key request

* refresh databases in test instead of product code

* when refreshing containers, open console window to check for status completion

* close notification console window after seeing desired log

* create and delete a container for each individual test

* dont delete database after every test. leave it to the CI

* Don't refresh databases when opening Scale+Settings and only delete database if running locally

* only open scale and settings at the beginning of each test suite

* get it back to working

* change settings.spect.ts from serial to parallel

* don't delete database after each test

* update container creation throughpout to be 5000

* run tests with no throughput limit on the account

* adjust scale test to reflect no throughput limit on account

* remove test container throughput

* don't refresh collections when clicking settings in product code

* refactor and run cleanup during pr check

* copy cleanup accounts

* run cleanup after playwright tests

* run cleanup every three hours

* revert ci.yml

* update cpk test

* remove cpk

* remove cleanup accounts and add cpk

* add cpk

* remove cpk changes

* revert ci.yml

* run cleanup every two hours

---------

Co-authored-by: Jade Welton <jawelton@microsoft.com>
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2026-01-07 00:36:54 -05:00
BChoudhury-ms
53288dec6f feat: add test identifiers (data-test) to Container Copy components (#2306) 2026-01-06 21:19:58 +05:30
BChoudhury-ms
258a6286e7 refactor(dataTransfers): replace fetch with armRequest for pagination (#2305)
* refactor(dataTransfers): replace fetch with armRequest for pagination

* added comment for next link url parsing
2026-01-06 21:12:19 +05:30
sakshigupta12feb
c8ebca6da4 Fixed homepage UI for fabric (#2304)
* Fixed homePage UI for fabric


Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-01-05 17:02:19 +05:30
BChoudhury-ms
6167f94bc3 fix: restore SidePanel component for Container Copy feature (#2295) 2025-12-29 21:46:47 +05:30
BChoudhury-ms
be89c634f3 Add E2E tests for partition key change workflow (#2293) 2025-12-29 15:08:54 +05:30
Asier Isayas
bc43d3afbb add user defined function playwright test 2025-12-22 10:21:22 -08:00
Asier Isayas
6691974127 stored procedure playwright test 2025-12-19 10:25:45 -08:00
Asier Isayas
37a5663bad format 2025-12-17 11:35:28 -08:00
Asier Isayas
31c9574df4 cleanup 2025-12-17 11:32:55 -08:00
Asier Isayas
2e8e02f75b nit 2025-12-17 11:24:42 -08:00
Asier Isayas
8809ba6cd2 fix tests 2025-12-17 11:21:39 -08:00
Asier Isayas
592f2cd7e6 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas-playwright-2 2025-12-17 11:09:08 -08:00
Asier Isayas
d966f4f2a7 Add more playwright tests 2025-12-17 11:03:28 -08:00
sakshigupta12feb
42e230b88b Few more UI observation (fixes) (#2283)
* fixed bottom border for fabric

* fixed scrollbar

* reverted last

* updated the review comments

* Fixed scroll , updated the home page UI box shadow, header font weight, margin between boxed , subtab underline for fabric fixed

---------

Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2025-12-17 23:54:30 +05:30
sakshigupta12feb
6196ba4722 Fixed bottom border for fabric and small UI changes (#2282)
* fixed bottom border for fabric

* updated the review comments

---------

Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2025-12-16 21:35:08 +05:30
Asier Isayas
4801aae754 changed throughput above limit 2025-12-08 11:21:20 -08:00
Asier Isayas
2a02112d87 fix autoscale selector 2025-12-08 09:55:21 -08:00
Asier Isayas
bbfff77495 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/playwright 2025-12-08 07:39:33 -08:00
Asier Isayas
f695b42071 fix tests 2025-12-04 12:24:47 -08:00
Asier Isayas
a8a96e22b4 fix unit tests 2025-12-04 12:05:47 -08:00
Asier Isayas
a1b026544d fix unit tests and lint 2025-12-04 11:52:31 -08:00
Asier Isayas
a912233b33 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/playwright 2025-12-04 11:43:17 -08:00
Asier Isayas
487130f6e3 Add playwright tests for Autoscale/Manual Throughpout and TTL 2025-12-04 11:40:48 -08:00
113 changed files with 2500 additions and 1694 deletions

View File

@@ -164,8 +164,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
shardTotal: [16] shardTotal: [20]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Node.js 18.x - 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) 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 "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV 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) 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 "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV 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) 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 "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV 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) # 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 "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV # 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) # 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 "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV # 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) # 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 "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV # 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) # 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 "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV # 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']}} - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3 run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
- name: Upload blob report to GitHub Actions Artifacts - name: Upload blob report to GitHub Actions Artifacts

View File

@@ -6,8 +6,8 @@ on:
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
schedule: schedule:
# Once every hour # Once every two hours
- cron: "0 15 * * *" - cron: "0 */2 * * *"
permissions: permissions:
id-token: write id-token: write

View File

@@ -128,7 +128,7 @@
@provisionDatabaseThroughputInfo: 200px; @provisionDatabaseThroughputInfo: 200px;
//tabs container //tabs container
@ActiveTabHeight: 31px; @ActiveTabHeight: 32px;
@ActiveTabWidth: 141px; @ActiveTabWidth: 141px;
@TabsHeight: 30px; @TabsHeight: 30px;
@TabsWidth: 140px; @TabsWidth: 140px;

View File

@@ -406,7 +406,11 @@ body {
width: 440px; width: 440px;
min-height: 565px; min-height: 565px;
} }
.dataExplorerLoaderforcopyJobs{
width: 100%;
min-height: 565px;
right: 0;
}
.dataExplorerTabLoaderContainer { .dataExplorerTabLoaderContainer {
left: initial; left: initial;
top: initial; top: initial;
@@ -2643,7 +2647,7 @@ a:link {
.tabPanesContainer { .tabPanesContainer {
flex-grow: 1; flex-grow: 1;
overflow-y: scroll; overflow: hidden;
background-color: var(--colorNeutralBackground1); background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1); color: var(--colorNeutralForeground1);
} }
@@ -2651,6 +2655,7 @@ a:link {
.tabs-container { .tabs-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow-y: auto;
} }
.paddingspan4 { .paddingspan4 {
@@ -2677,7 +2682,7 @@ a:link {
width: @ActiveTabWidth; width: @ActiveTabWidth;
} }
.nav-tabs > li.active .contentWrapper { .nav-tabs > li.active .contentWrapper .tabNavText {
border-bottom: 2px solid var(--colorCompoundBrandBackground); border-bottom: 2px solid var(--colorCompoundBrandBackground);
} }

View File

@@ -7,6 +7,7 @@ html {
body { body {
font-family: @FabricFont; font-family: @FabricFont;
background-color: #f5f5f5; background-color: #f5f5f5;
--colorCompoundBrandBackground: @FabricAccentMedium;
} }
a { a {
@@ -41,7 +42,7 @@ a:focus {
} }
.nav-tabs-margin { .nav-tabs-margin {
padding-top: 5px; padding-top: 0px;
background-color: #ffffff; background-color: #ffffff;
} }
@@ -68,17 +69,20 @@ a:focus {
} }
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover { .nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
border-bottom: 2px solid #e0e0e0; border-bottom: none;
} }
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content, .nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover { .nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
border-bottom: 2px solid @FabricAccentMedium; border-bottom: none;
} }
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText { .nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
border-bottom: 0px none transparent; border-bottom: 0px none transparent;
} }
.nav-tabs > li.active .contentWrapper .tabNavText {
border-bottom: 2px solid @FabricAccentMedium;
}
.tabNavContentContainer { .tabNavContentContainer {
padding: @SmallSpace 0px @SmallSpace 0px; padding: @SmallSpace 0px @SmallSpace 0px;
@@ -214,6 +218,7 @@ a:focus {
.tabPanesContainer { .tabPanesContainer {
overflow: auto !important; overflow: auto !important;
display: flex;
} }
.tabs-container { .tabs-container {

2
package-lock.json generated
View File

@@ -116,8 +116,8 @@
"tinykeys": "2.1.0", "tinykeys": "2.1.0",
"underscore": "1.12.1", "underscore": "1.12.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"web-vitals": "4.2.4",
"uuid": "9.0.0", "uuid": "9.0.0",
"web-vitals": "4.2.4",
"zustand": "3.5.0" "zustand": "3.5.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -11,8 +11,8 @@ export default defineConfig({
reporter: process.env.CI ? "blob" : "html", reporter: process.env.CI ? "blob" : "html",
timeout: 10 * 60 * 1000, timeout: 10 * 60 * 1000,
use: { use: {
trace: "off", trace: "retain-on-failure",
video: "off", video: "retain-on-failure",
screenshot: "on", screenshot: "on",
testIdAttribute: "data-test", testIdAttribute: "data-test",
contextOptions: { contextOptions: {

View File

@@ -1,4 +1,5 @@
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react"; import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
import { useThemeStore } from "hooks/useTheme";
import React from "react"; import React from "react";
interface LoadingOverlayProps { interface LoadingOverlayProps {
@@ -7,15 +8,17 @@ interface LoadingOverlayProps {
} }
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => { const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
if (!isLoading) { if (!isLoading) {
return null; return null;
} }
return ( return (
<Overlay <Overlay
data-test="loading-overlay"
styles={{ styles={{
root: { root: {
backgroundColor: "rgba(255,255,255,0.9)", backgroundColor: isDarkMode ? "rgba(32, 31, 30, 0.9)" : "rgba(255,255,255,0.9)",
zIndex: 9999, zIndex: 9999,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@@ -23,7 +26,11 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
}, },
}} }}
> >
<Spinner size={SpinnerSize.large} label={label} styles={{ label: { fontWeight: 600 } }} /> <Spinner
size={SpinnerSize.large}
label={label}
styles={{ label: { fontWeight: 600, color: isDarkMode ? "#ffffff" : "#323130" } }}
/>
</Overlay> </Overlay>
); );
}; };

View File

@@ -11,3 +11,14 @@
gap: 8px; gap: 8px;
align-items: center; 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);
}

View File

@@ -59,7 +59,7 @@ const Pager: React.FC<PagerProps> = ({
return ( return (
<div className={className || "pager-container"}> <div className={className || "pager-container"}>
{showItemCount && ( {showItemCount && (
<Text> <Text className="themeText">
Showing {startIndex + 1} - {endIndex} of {totalCount} items Showing {startIndex + 1} - {endIndex} of {totalCount} items
</Text> </Text>
)} )}
@@ -82,7 +82,7 @@ const Pager: React.FC<PagerProps> = ({
disabled={disabled || currentPage === 1} disabled={disabled || currentPage === 1}
styles={iconButtonStyles} styles={iconButtonStyles}
/> />
<Text> <Text className="themeText">
Page {currentPage} of {totalPages} Page {currentPage} of {totalPages}
</Text> </Text>
<IconButton <IconButton

View File

@@ -3,6 +3,7 @@
exports[`LoadingOverlay should handle long labels properly 1`] = ` exports[`LoadingOverlay should handle long labels properly 1`] = `
<div <div
class="ms-Overlay root-109" class="ms-Overlay root-109"
data-test="loading-overlay"
> >
<div <div
class="ms-Spinner root-111" class="ms-Spinner root-111"
@@ -22,6 +23,7 @@ exports[`LoadingOverlay should handle long labels properly 1`] = `
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = ` exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
<div <div
class="ms-Overlay root-109" class="ms-Overlay root-109"
data-test="loading-overlay"
> >
<div <div
class="ms-Spinner root-111" class="ms-Spinner root-111"
@@ -41,6 +43,7 @@ exports[`LoadingOverlay should render loading overlay when isLoading is true 1`]
exports[`LoadingOverlay should render loading overlay with custom label 1`] = ` exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
<div <div
class="ms-Overlay root-109" class="ms-Overlay root-109"
data-test="loading-overlay"
> >
<div <div
class="ms-Spinner root-111" class="ms-Spinner root-111"
@@ -60,6 +63,7 @@ exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
exports[`LoadingOverlay should render loading overlay with empty label 1`] = ` exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
<div <div
class="ms-Overlay root-109" class="ms-Overlay root-109"
data-test="loading-overlay"
> >
<div <div
class="ms-Spinner root-111" class="ms-Spinner root-111"

View File

@@ -9,7 +9,7 @@ import {
SqlStoredProcedureCreateUpdateParameters, SqlStoredProcedureCreateUpdateParameters,
SqlStoredProcedureResource, SqlStoredProcedureResource,
} from "../../Utils/arm/generatedClients/cosmos/types"; } from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
@@ -20,6 +20,7 @@ export async function createStoredProcedure(
): Promise<StoredProcedureDefinition & Resource> { ): Promise<StoredProcedureDefinition & Resource> {
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`); const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
try { try {
let resource: StoredProcedureDefinition & Resource;
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
@@ -60,14 +61,16 @@ export async function createStoredProcedure(
storedProcedure.id, storedProcedure.id,
createSprocParams, createSprocParams,
); );
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource); resource = rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
} } else {
const response = await client() const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.storedProcedures.create(storedProcedure); .scripts.storedProcedures.create(storedProcedure);
return response?.resource; resource = response.resource;
}
logConsoleInfo(`Successfully created stored procedure ${storedProcedure.id}`);
return resource;
} catch (error) { } catch (error) {
handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`); handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`);
throw error; throw error;

View File

@@ -3,7 +3,7 @@ import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
@@ -14,6 +14,7 @@ export async function createTrigger(
): Promise<TriggerDefinition | SqlTriggerResource> { ): Promise<TriggerDefinition | SqlTriggerResource> {
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`); const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
try { try {
let resource: SqlTriggerResource | TriggerDefinition;
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
@@ -52,14 +53,16 @@ export async function createTrigger(
trigger.id, trigger.id,
createTriggerParams, createTriggerParams,
); );
return rpResponse && rpResponse.properties?.resource; resource = rpResponse && rpResponse.properties?.resource;
} } else {
const sdkResponse = await client()
const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.triggers.create(trigger as unknown as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type .scripts.triggers.create(trigger as unknown as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type
return response.resource; resource = sdkResponse.resource;
}
logConsoleInfo(`Successfully created trigger ${trigger.id}`);
return resource;
} catch (error) { } catch (error) {
handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`); handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`);
throw error; throw error;

View File

@@ -9,7 +9,7 @@ import {
SqlUserDefinedFunctionCreateUpdateParameters, SqlUserDefinedFunctionCreateUpdateParameters,
SqlUserDefinedFunctionResource, SqlUserDefinedFunctionResource,
} from "../../Utils/arm/generatedClients/cosmos/types"; } from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
@@ -20,6 +20,7 @@ export async function createUserDefinedFunction(
): Promise<UserDefinedFunctionDefinition & Resource> { ): Promise<UserDefinedFunctionDefinition & Resource> {
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`); const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
try { try {
let resource: UserDefinedFunctionDefinition & Resource;
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
@@ -60,14 +61,17 @@ export async function createUserDefinedFunction(
userDefinedFunction.id, userDefinedFunction.id,
createUDFParams, createUDFParams,
); );
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
resource = rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
} else {
const response = await client() const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.userDefinedFunctions.create(userDefinedFunction); .scripts.userDefinedFunctions.create(userDefinedFunction);
return response?.resource; resource = response.resource;
}
logConsoleInfo(`Successfully created user defined function ${userDefinedFunction.id}`);
return resource;
} catch (error) { } catch (error) {
handleError( handleError(
error, error,

View File

@@ -1,3 +1,4 @@
import { configContext } from "ConfigContext";
import { ApiType, userContext } from "UserContext"; import { ApiType, userContext } from "UserContext";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { import {
@@ -14,9 +15,12 @@ import {
DataTransferJobFeedResults, DataTransferJobFeedResults,
DataTransferJobGetResults, DataTransferJobGetResults,
} from "Utils/arm/generatedClients/dataTransferService/types"; } from "Utils/arm/generatedClients/dataTransferService/types";
import { armRequest } from "Utils/arm/request";
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs"; import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry"; import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
export const DATA_TRANSFER_JOB_API_VERSION = "2025-05-01-preview";
export interface DataTransferParams { export interface DataTransferParams {
jobName: string; jobName: string;
apiType: ApiType; apiType: ApiType;
@@ -33,26 +37,34 @@ export const getDataTransferJobs = async (
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
signal?: AbortSignal,
): Promise<DataTransferJobGetResults[]> => { ): Promise<DataTransferJobGetResults[]> => {
let dataTransferJobs: DataTransferJobGetResults[] = []; let dataTransferJobs: DataTransferJobGetResults[] = [];
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount( let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
accountName, accountName,
signal,
); );
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])]; dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
while (dataTransferFeeds?.nextLink) { while (dataTransferFeeds?.nextLink) {
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, { /**
headers: { * The `nextLink` URL returned by the Cosmos DB SQL API pointed to an incorrect endpoint, causing timeouts.
Authorization: userContext.authorizationToken, * (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) { dataTransferJobs.push(...(dataTransferFeeds?.value || []));
dataTransferFeeds = await nextResponse.json();
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
} else {
break;
}
} }
return dataTransferJobs; return dataTransferJobs;
}; };

View File

@@ -28,6 +28,7 @@ export async function deleteStoredProcedure(
} else { } else {
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete(); await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
} }
logConsoleProgress(`Successfully deleted stored procedure ${storedProcedureId}`);
} catch (error) { } catch (error) {
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`); handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
throw error; throw error;

View File

@@ -24,6 +24,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
} else { } else {
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete(); await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
} }
logConsoleProgress(`Successfully deleted trigger ${triggerId}`);
} catch (error) { } catch (error) {
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`); handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
throw error; throw error;

View File

@@ -24,6 +24,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
} else { } else {
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete(); await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
} }
logConsoleProgress(`Successfully deleted user defined function ${id}`);
} catch (error) { } catch (error) {
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`); handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
throw error; throw error;

View File

@@ -1,5 +1,6 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
import * as Logger from "../../../Common/Logger"; import * as Logger from "../../../Common/Logger";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs"; 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("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState"); jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
jest.mock("../CopyJobUtils"); jest.mock("../CopyJobUtils");
jest.mock("../../../Common/dataAccess/dataTransfers");
describe("CopyJobActions", () => { describe("CopyJobActions", () => {
beforeEach(() => { beforeEach(() => {
@@ -154,8 +156,7 @@ describe("CopyJobActions", () => {
}); });
it("should fetch and format copy jobs successfully", async () => { it("should fetch and format copy jobs successfully", async () => {
const mockResponse = { const mockResponse = [
value: [
{ {
properties: { properties: {
jobName: "job-1", jobName: "job-1",
@@ -177,10 +178,9 @@ describe("CopyJobActions", () => {
}, },
}, },
}, },
], ];
};
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse); (getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({ (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM", formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000, timestamp: 1704106800000,
@@ -201,8 +201,7 @@ describe("CopyJobActions", () => {
}); });
it("should filter jobs by CosmosDBSql component", async () => { it("should filter jobs by CosmosDBSql component", async () => {
const mockResponse = { const mockResponse = [
value: [
{ {
properties: { properties: {
jobName: "sql-job", jobName: "sql-job",
@@ -229,10 +228,9 @@ describe("CopyJobActions", () => {
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, 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({ (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM", formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000, timestamp: 1704106800000,
@@ -247,8 +245,7 @@ describe("CopyJobActions", () => {
}); });
it("should sort jobs by last updated time (newest first)", async () => { it("should sort jobs by last updated time (newest first)", async () => {
const mockResponse = { const mockResponse = [
value: [
{ {
properties: { properties: {
jobName: "older-job", jobName: "older-job",
@@ -275,10 +272,9 @@ describe("CopyJobActions", () => {
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" }, 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({ (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM", formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000, timestamp: 1704106800000,
@@ -293,8 +289,7 @@ describe("CopyJobActions", () => {
}); });
it("should calculate completion percentage correctly", async () => { it("should calculate completion percentage correctly", async () => {
const mockResponse = { const mockResponse = [
value: [
{ {
properties: { properties: {
jobName: "job-1", jobName: "job-1",
@@ -308,10 +303,9 @@ describe("CopyJobActions", () => {
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, 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({ (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM", formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000, timestamp: 1704106800000,
@@ -325,8 +319,7 @@ describe("CopyJobActions", () => {
}); });
it("should handle zero total count gracefully", async () => { it("should handle zero total count gracefully", async () => {
const mockResponse = { const mockResponse = [
value: [
{ {
properties: { properties: {
jobName: "job-1", jobName: "job-1",
@@ -340,10 +333,9 @@ describe("CopyJobActions", () => {
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" }, 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({ (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM", formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000, timestamp: 1704106800000,
@@ -361,8 +353,7 @@ describe("CopyJobActions", () => {
message: "Error message line 1\r\n\r\nError message line 2", message: "Error message line 1\r\n\r\nError message line 2",
code: "ErrorCode123", code: "ErrorCode123",
}; };
const mockResponse = { const mockResponse = [
value: [
{ {
properties: { properties: {
jobName: "failed-job", jobName: "failed-job",
@@ -377,10 +368,9 @@ describe("CopyJobActions", () => {
error: mockError, error: mockError,
}, },
}, },
], ];
};
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse); (getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({ (CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM", formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000, timestamp: 1704106800000,
@@ -408,7 +398,7 @@ describe("CopyJobActions", () => {
}; };
(global as any).AbortController = jest.fn(() => mockAbortController); (global as any).AbortController = jest.fn(() => mockAbortController);
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ value: [] }); (getDataTransferJobs as jest.Mock).mockResolvedValue([]);
getCopyJobs(); getCopyJobs();
expect(mockAbortController.abort).not.toHaveBeenCalled(); expect(mockAbortController.abort).not.toHaveBeenCalled();
@@ -418,9 +408,7 @@ describe("CopyJobActions", () => {
}); });
it("should throw error for invalid response format", async () => { it("should throw error for invalid response format", async () => {
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ (getDataTransferJobs as jest.Mock).mockResolvedValue("not-an-array");
value: "not-an-array",
});
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs."); await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
}); });
@@ -430,7 +418,7 @@ describe("CopyJobActions", () => {
message: "Aborted", message: "Aborted",
content: JSON.stringify({ message: "signal is aborted without reason" }), 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({ await expect(getCopyJobs()).rejects.toMatchObject({
message: expect.stringContaining("Previous copy job request was cancelled."), message: expect.stringContaining("Previous copy job request was cancelled."),
@@ -439,7 +427,7 @@ describe("CopyJobActions", () => {
it("should handle generic errors", async () => { it("should handle generic errors", async () => {
const genericError = new Error("Network error"); 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"); await expect(getCopyJobs()).rejects.toThrow("Network error");
}); });

View File

@@ -1,13 +1,13 @@
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import React from "react"; import React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
import { logError } from "../../../Common/Logger"; import { logError } from "../../../Common/Logger";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { import {
cancel, cancel,
complete, complete,
create, create,
listByDatabaseAccount,
pause, pause,
resume, resume,
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs"; } from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
@@ -63,14 +63,8 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId( const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "", userContext.databaseAccount?.id || "",
); );
const response = await listByDatabaseAccount( const jobs = await getDataTransferJobs(subscriptionId, resourceGroup, accountName, copyJobsAbortController.signal);
subscriptionId,
resourceGroup,
accountName,
copyJobsAbortController.signal,
);
const jobs = response.value || [];
if (!Array.isArray(jobs)) { if (!Array.isArray(jobs)) {
throw new Error("Invalid migration job status response: Expected an array of jobs."); throw new Error("Invalid migration job status response: Expected an array of jobs.");
} }

View File

@@ -39,7 +39,7 @@ describe("CopyJobCommandBar", () => {
render(<CopyJobCommandBar explorer={mockExplorer} />); render(<CopyJobCommandBar explorer={mockExplorer} />);
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer); expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1); expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
}); });
@@ -163,7 +163,7 @@ describe("CopyJobCommandBar", () => {
render(<CopyJobCommandBar explorer={mockExplorer} />); render(<CopyJobCommandBar explorer={mockExplorer} />);
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer); expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps); expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
}); });
@@ -175,11 +175,11 @@ describe("CopyJobCommandBar", () => {
mockConvertButton.mockReturnValue([]); mockConvertButton.mockReturnValue([]);
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />); const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1); expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1, false);
rerender(<CopyJobCommandBar explorer={mockExplorer2} />); rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2); expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2, false);
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2); expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
}); });
}); });

View File

@@ -1,24 +1,28 @@
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import React from "react"; import React from "react";
import { StyleConstants } from "../../../Common/StyleConstants"; import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil"; import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
import { getThemeTokens } from "../../Theme/ThemeUtil";
import { ContainerCopyProps } from "../Types/CopyJobTypes"; import { ContainerCopyProps } from "../Types/CopyJobTypes";
import { getCommandBarButtons } from "./Utils"; import { getCommandBarButtons } from "./Utils";
const backgroundColor = StyleConstants.BaseLight; const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
const themeTokens = getThemeTokens(isDarkMode);
const backgroundColor = themeTokens.colorNeutralBackground1;
const rootStyle = { const rootStyle = {
root: { root: {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
}, },
}; };
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => { const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer, isDarkMode);
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer);
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor); const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
return ( return (
<div className="commandBarContainer"> <div className="commandBarContainer" style={{ backgroundColor }}>
<FluentCommandBar <FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands" ariaLabel="Use left and right arrow keys to navigate between commands"
styles={rootStyle} styles={rootStyle}

View File

@@ -50,7 +50,7 @@ describe("CommandBar Utils", () => {
describe("getCommandBarButtons", () => { describe("getCommandBarButtons", () => {
it("should return an array of command button props", () => { it("should return an array of command button props", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
expect(buttons).toBeDefined(); expect(buttons).toBeDefined();
expect(Array.isArray(buttons)).toBe(true); expect(Array.isArray(buttons)).toBe(true);
@@ -58,7 +58,7 @@ describe("CommandBar Utils", () => {
}); });
it("should include create copy job button", () => { it("should include create copy job button", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
const createButton = buttons[0]; const createButton = buttons[0];
expect(createButton).toBeDefined(); expect(createButton).toBeDefined();
@@ -70,7 +70,7 @@ describe("CommandBar Utils", () => {
}); });
it("should include refresh button", () => { it("should include refresh button", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
const refreshButton = buttons[1]; const refreshButton = buttons[1];
expect(refreshButton).toBeDefined(); expect(refreshButton).toBeDefined();
@@ -80,11 +80,11 @@ describe("CommandBar Utils", () => {
}); });
it("should include feedback button when platform is Portal", () => { 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).toBeDefined();
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs"); expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
expect(feedbackButton.tooltipText).toBe("Feedback"); expect(feedbackButton.tooltipText).toBe("Feedback");
@@ -105,13 +105,13 @@ describe("CommandBar Utils", () => {
})); }));
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./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", () => { it("should call openCreateCopyJobPanel when create button is clicked", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
const createButton = buttons[0]; const createButton = buttons[0];
createButton.onCommandClick({} as React.SyntheticEvent); createButton.onCommandClick({} as React.SyntheticEvent);
@@ -121,7 +121,7 @@ describe("CommandBar Utils", () => {
}); });
it("should call refreshJobList when refresh button is clicked", () => { it("should call refreshJobList when refresh button is clicked", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
const refreshButton = buttons[1]; const refreshButton = buttons[1];
refreshButton.onCommandClick({} as React.SyntheticEvent); refreshButton.onCommandClick({} as React.SyntheticEvent);
@@ -130,8 +130,8 @@ describe("CommandBar Utils", () => {
}); });
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => { it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
const feedbackButton = buttons[2]; const feedbackButton = buttons[3];
feedbackButton.onCommandClick({} as React.SyntheticEvent); feedbackButton.onCommandClick({} as React.SyntheticEvent);
@@ -139,7 +139,7 @@ describe("CommandBar Utils", () => {
}); });
it("should return buttons with correct icon sources", () => { 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].iconSrc).toBeDefined();
expect(buttons[0].iconAlt).toBe("Create Copy Job"); expect(buttons[0].iconAlt).toBe("Create Copy Job");
@@ -148,7 +148,10 @@ describe("CommandBar Utils", () => {
expect(buttons[1].iconAlt).toBe("Refresh"); expect(buttons[1].iconAlt).toBe("Refresh");
expect(buttons[2].iconSrc).toBeDefined(); 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", () => { it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
@@ -157,14 +160,14 @@ describe("CommandBar Utils", () => {
return selector(state); return selector(state);
}); });
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
const refreshButton = buttons[1]; const refreshButton = buttons[1];
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow(); expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
}); });
it("should set hasPopup to false for all buttons", () => { it("should set hasPopup to false for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { buttons.forEach((button) => {
expect(button.hasPopup).toBe(false); expect(button.hasPopup).toBe(false);
@@ -172,7 +175,7 @@ describe("CommandBar Utils", () => {
}); });
it("should set commandButtonLabel to undefined for all buttons", () => { it("should set commandButtonLabel to undefined for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { buttons.forEach((button) => {
expect(button.commandButtonLabel).toBeUndefined(); expect(button.commandButtonLabel).toBeUndefined();
@@ -180,7 +183,7 @@ describe("CommandBar Utils", () => {
}); });
it("should respect disabled state when provided", () => { it("should respect disabled state when provided", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { buttons.forEach((button) => {
expect(button.disabled).toBe(false); expect(button.disabled).toBe(false);
@@ -188,7 +191,7 @@ describe("CommandBar Utils", () => {
}); });
it("should return CommandButtonComponentProps with all required properties", () => { it("should return CommandButtonComponentProps with all required properties", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button: CommandButtonComponentProps) => { buttons.forEach((button: CommandButtonComponentProps) => {
expect(button).toHaveProperty("iconSrc"); expect(button).toHaveProperty("iconSrc");
@@ -202,18 +205,19 @@ describe("CommandBar Utils", () => {
}); });
}); });
it("should maintain button order: create, refresh, feedback", () => { it("should maintain button order: create, refresh, themeToggle, feedback", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
expect(buttons[0].tooltipText).toBe("Create Copy Job"); expect(buttons[0].tooltipText).toBe("Create Copy Job");
expect(buttons[1].tooltipText).toBe("Refresh"); 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", () => { describe("Button click handlers", () => {
it("should execute click handlers without errors", () => { it("should execute click handlers without errors", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { buttons.forEach((button) => {
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow(); expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
@@ -221,7 +225,7 @@ describe("CommandBar Utils", () => {
}); });
it("should call correct action for each button", () => { it("should call correct action for each button", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons[0].onCommandClick({} as React.SyntheticEvent); buttons[0].onCommandClick({} as React.SyntheticEvent);
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer); expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
@@ -229,14 +233,14 @@ describe("CommandBar Utils", () => {
buttons[1].onCommandClick({} as React.SyntheticEvent); buttons[1].onCommandClick({} as React.SyntheticEvent);
expect(mockRefreshJobList).toHaveBeenCalled(); expect(mockRefreshJobList).toHaveBeenCalled();
buttons[2].onCommandClick({} as React.SyntheticEvent); buttons[3].onCommandClick({} as React.SyntheticEvent);
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled(); expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
}); });
}); });
describe("Accessibility", () => { describe("Accessibility", () => {
it("should have aria labels for all buttons", () => { it("should have aria labels for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { buttons.forEach((button) => {
expect(button.ariaLabel).toBeDefined(); expect(button.ariaLabel).toBeDefined();
@@ -246,7 +250,7 @@ describe("CommandBar Utils", () => {
}); });
it("should have tooltip text for all buttons", () => { it("should have tooltip text for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { buttons.forEach((button) => {
expect(button.tooltipText).toBeDefined(); expect(button.tooltipText).toBeDefined();
@@ -256,7 +260,7 @@ describe("CommandBar Utils", () => {
}); });
it("should have icon alt text for all buttons", () => { it("should have icon alt text for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { buttons.forEach((button) => {
expect(button.iconAlt).toBeDefined(); expect(button.iconAlt).toBeDefined();

View File

@@ -1,7 +1,10 @@
import AddIcon from "../../../../images/Add.svg"; import AddIcon from "../../../../images/Add.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import MoonIcon from "../../../../images/MoonIcon.svg";
import RefreshIcon from "../../../../images/refresh-cosmos.svg"; import RefreshIcon from "../../../../images/refresh-cosmos.svg";
import SunIcon from "../../../../images/SunIcon.svg";
import { configContext, Platform } from "../../../ConfigContext"; import { configContext, Platform } from "../../../ConfigContext";
import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as Actions from "../Actions/CopyJobActions"; import * as Actions from "../Actions/CopyJobActions";
@@ -9,7 +12,7 @@ import ContainerCopyMessages from "../ContainerCopyMessages";
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState"; import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes"; import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] { function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref); const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
const buttons: CopyJobCommandBarBtnType[] = [ const buttons: CopyJobCommandBarBtnType[] = [
{ {
@@ -26,7 +29,15 @@ function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel, ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
onClick: () => monitorCopyJobsRef?.refreshJobList(), 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) { if (configContext.platform === Platform.Portal) {
buttons.push({ buttons.push({
key: "feedback", key: "feedback",
@@ -54,6 +65,6 @@ function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProp
}; };
} }
export function getCommandBarButtons(explorer: Explorer): CommandButtonComponentProps[] { export function getCommandBarButtons(explorer: Explorer, isDarkMode: boolean): CommandButtonComponentProps[] {
return getCopyJobBtns(explorer).map(btnMapper); return getCopyJobBtns(explorer, isDarkMode).map(btnMapper);
} }

View File

@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ( const managedIdentityTooltip = (
<Text> <Text>
{ContainerCopyMessages.addManagedIdentity.tooltip.content} &nbsp; {ContainerCopyMessages.addManagedIdentity.tooltip.content} &nbsp;
<Link href={ContainerCopyMessages.addManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer"> <Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.addManagedIdentity.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText} {ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
</Link> </Link>
</Text> </Text>
@@ -26,7 +31,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
return ( return (
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Text> <Text className="themeText">
{ContainerCopyMessages.addManagedIdentity.description}&ensp; {ContainerCopyMessages.addManagedIdentity.description}&ensp;
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer"> <Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer">
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText} {ContainerCopyMessages.addManagedIdentity.descriptionHrefText}
@@ -35,6 +40,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<InfoTooltip content={managedIdentityTooltip} /> <InfoTooltip content={managedIdentityTooltip} />
</Text> </Text>
<Toggle <Toggle
data-test="btn-toggle"
checked={systemAssigned} checked={systemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText} offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -13,7 +13,12 @@ import useToggle from "./hooks/useToggle";
const TooltipContent = ( const TooltipContent = (
<Text> <Text>
{ContainerCopyMessages.readPermissionAssigned.tooltip.content} &nbsp; {ContainerCopyMessages.readPermissionAssigned.tooltip.content} &nbsp;
<Link href={ContainerCopyMessages.readPermissionAssigned.tooltip.href} target="_blank" rel="noopener noreferrer"> <Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText} {ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
</Link> </Link>
</Text> </Text>
@@ -65,6 +70,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
<InfoTooltip content={TooltipContent} /> <InfoTooltip content={TooltipContent} />
</Text> </Text>
<Toggle <Toggle
data-test="btn-toggle"
checked={readPermissionAssigned} checked={readPermissionAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText} offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -12,7 +12,7 @@ import { useCopyJobPrerequisitesCache } from "../../Utils/useCopyJobPrerequisite
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection"; import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => ( const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
<AccordionItem key={id} value={id} disabled={disabled}> <AccordionItem key={id} value={id} disabled={disabled} data-test="accordion-item">
<AccordionHeader className="accordionHeader"> <AccordionHeader className="accordionHeader">
<Text className="accordionHeaderText" variant="medium"> <Text className="accordionHeaderText" variant="medium">
{title} {title}
@@ -25,13 +25,13 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Compo
height={completed ? 20 : 24} height={completed ? 20 : 24}
/> />
</AccordionHeader> </AccordionHeader>
<AccordionPanel aria-disabled={disabled} className="accordionPanel"> <AccordionPanel aria-disabled={disabled} className="accordionPanel" data-test="accordion-panel">
<Component /> <Component />
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
); );
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description, sections }) => { const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, description, sections }) => {
const [openItems, setOpenItems] = React.useState<string[]>([]); const [openItems, setOpenItems] = React.useState<string[]>([]);
useEffect(() => { useEffect(() => {
@@ -44,11 +44,12 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description,
return ( return (
<Stack <Stack
data-test={`permission-group-container-${id}`}
tokens={{ childrenGap: 15 }} tokens={{ childrenGap: 15 }}
styles={{ styles={{
root: { root: {
background: "#fafafa", background: "var(--colorNeutralBackground2)",
border: "1px solid #e1e1e1", border: "1px solid var(--colorNeutralStroke1)",
borderRadius: 8, borderRadius: 8,
padding: 16, padding: 16,
boxShadow: "0 1px 3px rgba(0,0,0,0.1)", boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
@@ -56,11 +57,11 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description,
}} }}
> >
<Stack tokens={{ childrenGap: 5 }}> <Stack tokens={{ childrenGap: 5 }}>
<Text variant="medium" style={{ fontWeight: 600 }}> <Text variant="medium" style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
{title} {title}
</Text> </Text>
{description && ( {description && (
<Text variant="small" styles={{ root: { color: "#605E5C" } }}> <Text variant="small" styles={{ root: { color: "var(--colorNeutralForeground2)" } }}>
{description} {description}
</Text> </Text>
)} )}
@@ -99,8 +100,12 @@ const AssignPermissions = () => {
}, []); }, []);
return ( return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}> <Stack
<Text variant="medium"> data-test="Panel:AssignPermissionsContainer"
className="assignPermissionsContainer"
tokens={{ childrenGap: 20 }}
>
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online {isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription( ? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
copyJobState?.source?.account?.name || "", copyJobState?.source?.account?.name || "",

View File

@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ( const managedIdentityTooltip = (
<Text> <Text>
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content} &nbsp; {ContainerCopyMessages.defaultManagedIdentity.tooltip.content} &nbsp;
<Link href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer"> <Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText} {ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
</Link> </Link>
</Text> </Text>
@@ -31,6 +36,7 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<InfoTooltip content={managedIdentityTooltip} /> <InfoTooltip content={managedIdentityTooltip} />
</div> </div>
<Toggle <Toggle
data-test="btn-toggle"
checked={defaultSystemAssigned} checked={defaultSystemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText} offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -13,7 +13,12 @@ import InfoTooltip from "../Components/InfoTooltip";
const tooltipContent = ( const tooltipContent = (
<Text> <Text>
{ContainerCopyMessages.pointInTimeRestore.tooltip.content} &nbsp; {ContainerCopyMessages.pointInTimeRestore.tooltip.content} &nbsp;
<Link href={ContainerCopyMessages.pointInTimeRestore.tooltip.href} target="_blank" rel="noopener noreferrer"> <Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.pointInTimeRestore.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText} {ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
</Link> </Link>
</Text> </Text>
@@ -127,6 +132,7 @@ const PointInTimeRestore: React.FC = () => {
<Stack.Item> <Stack.Item>
{showRefreshButton ? ( {showRefreshButton ? (
<PrimaryButton <PrimaryButton
data-test="pointInTimeRestore:RefreshBtn"
className="fullWidth" className="fullWidth"
text={ContainerCopyMessages.refreshButtonLabel} text={ContainerCopyMessages.refreshButtonLabel}
iconProps={{ iconName: "Refresh" }} iconProps={{ iconName: "Refresh" }}
@@ -134,6 +140,7 @@ const PointInTimeRestore: React.FC = () => {
/> />
) : ( ) : (
<PrimaryButton <PrimaryButton
data-test="pointInTimeRestore:PrimaryBtn"
className="fullWidth" className="fullWidth"
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText} text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})} {...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}

View File

@@ -5,7 +5,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
class="ms-Stack addManagedIdentityContainer css-109" class="ms-Stack addManagedIdentityContainer css-109"
> >
<span <span
class="css-110" class="themeText css-110"
> >
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 dont have to store any credentials in code. 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 dont 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" class="ms-Toggle-background pill-117"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle1" id="Toggle1"
role="switch" role="switch"
type="button" type="button"
@@ -92,7 +93,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
class="ms-Stack addManagedIdentityContainer css-109" class="ms-Stack addManagedIdentityContainer css-109"
> >
<span <span
class="css-110" class="themeText css-110"
> >
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 dont have to store any credentials in code. 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 dont 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" class="ms-Toggle-background pill-121"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle11" id="Toggle11"
role="switch" role="switch"
type="button" type="button"
@@ -173,10 +175,12 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
</div> </div>
<div <div
class="ms-Stack popover-container foreground loading css-123" class="ms-Stack popover-container foreground loading css-123"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<div <div
class="ms-Overlay root-135" class="ms-Overlay root-135"
data-test="loading-overlay"
> >
<div <div
class="ms-Spinner root-137" class="ms-Spinner root-137"
@@ -192,13 +196,13 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
</div> </div>
</div> </div>
<span <span
class="css-124" class="themeText css-124"
style="font-weight: 600;" style="font-weight: 600;"
> >
Enable system assigned managed identity Enable system assigned managed identity
</span> </span>
<span <span
class="css-110" class="themeText css-110"
> >
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button. Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
</span> </span>
@@ -261,7 +265,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
class="ms-Stack addManagedIdentityContainer css-109" class="ms-Stack addManagedIdentityContainer css-109"
> >
<span <span
class="css-110" class="themeText css-110"
> >
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 dont have to store any credentials in code. 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 dont 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" class="ms-Toggle-background pill-121"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle3" id="Toggle3"
role="switch" role="switch"
type="button" type="button"
@@ -342,16 +347,17 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
</div> </div>
<div <div
class="ms-Stack popover-container foreground css-123" class="ms-Stack popover-container foreground css-123"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<span <span
class="css-124" class="themeText css-124"
style="font-weight: 600;" style="font-weight: 600;"
> >
Enable system assigned managed identity Enable system assigned managed identity
</span> </span>
<span <span
class="css-110" class="themeText css-110"
> >
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button. Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
</span> </span>

View File

@@ -41,6 +41,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
class="ms-Toggle-background pill-115" class="ms-Toggle-background pill-115"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle17" id="Toggle17"
role="switch" role="switch"
type="button" type="button"
@@ -103,6 +104,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
class="ms-Toggle-background pill-115" class="ms-Toggle-background pill-115"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle16" id="Toggle16"
role="switch" role="switch"
type="button" type="button"
@@ -165,6 +167,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115" class="ms-Toggle-background pill-115"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle3" id="Toggle3"
role="switch" role="switch"
type="button" type="button"
@@ -227,6 +230,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-119" class="ms-Toggle-background pill-119"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle1" id="Toggle1"
role="switch" role="switch"
type="button" type="button"
@@ -314,6 +318,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115" class="ms-Toggle-background pill-115"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle0" id="Toggle0"
role="switch" role="switch"
type="button" type="button"
@@ -376,6 +381,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115" class="ms-Toggle-background pill-115"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle2" id="Toggle2"
role="switch" role="switch"
type="button" type="button"

View File

@@ -4,6 +4,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"
@@ -15,6 +16,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
> >
<div <div
class="ms-Stack css-112" class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
> >
<div <div
class="ms-Stack css-113" class="ms-Stack css-113"
@@ -36,6 +38,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
> >
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -85,6 +88,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -134,6 +138,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<div <div
aria-disabled="false" aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu" class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
> >
<div> <div>
Incomplete Component Incomplete Component
@@ -142,6 +147,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw" class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -201,6 +207,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"
@@ -212,6 +219,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
> >
<div <div
class="ms-Stack css-112" class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
> >
<div <div
class="ms-Stack css-113" class="ms-Stack css-113"
@@ -233,6 +241,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
> >
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -282,6 +291,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -331,6 +341,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div <div
aria-disabled="false" aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu" class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
> >
<div> <div>
Incomplete Component Incomplete Component
@@ -339,6 +350,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw" class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -398,6 +410,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"
@@ -409,6 +422,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
> >
<div <div
class="ms-Stack css-112" class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
> >
<div <div
class="ms-Stack css-113" class="ms-Stack css-113"
@@ -430,6 +444,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
> >
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -479,6 +494,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -528,6 +544,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div <div
aria-disabled="false" aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu" class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
> >
<div> <div>
Incomplete Component Incomplete Component
@@ -536,6 +553,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw" class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -595,6 +613,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"
@@ -606,6 +625,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
> >
<div <div
class="ms-Stack css-112" class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
> >
<div <div
class="ms-Stack css-113" class="ms-Stack css-113"
@@ -627,6 +647,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
> >
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -676,6 +697,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -725,6 +747,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<div <div
aria-disabled="false" aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu" class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
> >
<div> <div>
Incomplete Component Incomplete Component
@@ -733,6 +756,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw" class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -792,6 +816,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"
@@ -803,6 +828,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
> >
<div <div
class="ms-Stack css-112" class="ms-Stack css-112"
data-test="permission-group-container-crossAccountConfigs"
> >
<div <div
class="ms-Stack css-113" class="ms-Stack css-113"
@@ -824,6 +850,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
> >
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -875,6 +902,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
</div> </div>
<div <div
class="ms-Stack css-112" class="ms-Stack css-112"
data-test="permission-group-container-onlineConfigs"
> >
<div <div
class="ms-Stack css-113" class="ms-Stack css-113"
@@ -896,6 +924,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
> >
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -945,6 +974,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
<div <div
aria-disabled="false" aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu" class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
> >
<div <div
data-testid="online-copy-enabled" data-testid="online-copy-enabled"
@@ -964,6 +994,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"
@@ -975,6 +1006,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
> >
<div <div
class="ms-Stack css-112" class="ms-Stack css-112"
data-test="permission-group-container-onlineConfigs"
> >
<div <div
class="ms-Stack css-113" class="ms-Stack css-113"
@@ -996,6 +1028,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
> >
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1045,6 +1078,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1094,6 +1128,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
<div <div
aria-disabled="false" aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu" class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
> >
<div <div
data-testid="online-copy-enabled" data-testid="online-copy-enabled"
@@ -1113,6 +1148,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"
@@ -1124,6 +1160,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
> >
<div <div
class="ms-Stack css-112" class="ms-Stack css-112"
data-test="permission-group-container-crossAccountConfigs"
> >
<div <div
class="ms-Stack css-113" class="ms-Stack css-113"
@@ -1145,6 +1182,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
> >
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1194,6 +1232,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
</div> </div>
<div <div
class="fui-AccordionItem" class="fui-AccordionItem"
data-test="accordion-item"
> >
<div <div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5" class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1243,6 +1282,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
<div <div
aria-disabled="false" aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu" class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
> >
<div <div
data-testid="add-read-permission" data-testid="add-read-permission"
@@ -1262,6 +1302,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"
@@ -1283,6 +1324,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<div> <div>
<div <div
class="ms-Stack assignPermissionsContainer css-109" class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
> >
<span <span
class="css-110" class="css-110"

View File

@@ -41,6 +41,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
class="ms-Toggle-background pill-115" class="ms-Toggle-background pill-115"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle14" id="Toggle14"
role="switch" role="switch"
type="button" type="button"
@@ -103,6 +104,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
class="ms-Toggle-background pill-115" class="ms-Toggle-background pill-115"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle15" id="Toggle15"
role="switch" role="switch"
type="button" type="button"
@@ -165,6 +167,7 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
class="ms-Toggle-background pill-119" class="ms-Toggle-background pill-119"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle10" id="Toggle10"
role="switch" role="switch"
type="button" type="button"
@@ -256,6 +259,7 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
class="ms-Toggle-background pill-115" class="ms-Toggle-background pill-115"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle0" id="Toggle0"
role="switch" role="switch"
type="button" type="button"
@@ -318,6 +322,7 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
class="ms-Toggle-background pill-119" class="ms-Toggle-background pill-119"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="btn-toggle"
id="Toggle7" id="Toggle7"
role="switch" role="switch"
type="button" type="button"

View File

@@ -56,6 +56,7 @@ exports[`PointInTimeRestore Initial Render should render correctly with default
<button <button
class="ms-Button ms-Button--primary fullWidth root-115" class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true" data-is-focusable="true"
data-test="pointInTimeRestore:PrimaryBtn"
type="button" type="button"
> >
<span <span
@@ -131,6 +132,7 @@ exports[`PointInTimeRestore Initial Render should render with empty account name
<button <button
class="ms-Button ms-Button--primary fullWidth root-115" class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true" data-is-focusable="true"
data-test="pointInTimeRestore:PrimaryBtn"
type="button" type="button"
> >
<span <span
@@ -161,6 +163,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
> >
<div <div
class="ms-Overlay root-123" class="ms-Overlay root-123"
data-test="loading-overlay"
> >
<div <div
class="ms-Spinner root-125" class="ms-Spinner root-125"
@@ -223,6 +226,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
aria-disabled="true" aria-disabled="true"
class="ms-Button ms-Button--primary is-disabled fullWidth root-128" class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
data-is-focusable="false" data-is-focusable="false"
data-test="pointInTimeRestore:PrimaryBtn"
disabled="" disabled=""
type="button" type="button"
> >
@@ -301,6 +305,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot with refresh button
<button <button
class="ms-Button ms-Button--primary fullWidth root-115" class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true" data-is-focusable="true"
data-test="pointInTimeRestore:RefreshBtn"
type="button" type="button"
> >
<span <span

View File

@@ -19,9 +19,21 @@ const NavigationControls: React.FC<NavigationControlsProps> = ({
isPreviousDisabled, isPreviousDisabled,
}) => ( }) => (
<Stack horizontal tokens={{ childrenGap: 20 }}> <Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} /> <PrimaryButton
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} /> data-test="copy-job-primary"
<DefaultButton text="Cancel" onClick={onCancel} /> text={primaryBtnText}
onClick={onPrimary}
allowDisabledFocus
disabled={isPrimaryDisabled}
/>
<DefaultButton
data-test="copy-job-previous"
text="Previous"
onClick={onPrevious}
allowDisabledFocus
disabled={isPreviousDisabled}
/>
<DefaultButton data-test="copy-job-cancel" text="Cancel" onClick={onCancel} />
</Stack> </Stack>
); );

View File

@@ -17,15 +17,16 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
({ isLoading = false, title, children, onPrimary, onCancel }) => { ({ isLoading = false, title, children, onPrimary, onCancel }) => {
return ( return (
<Stack <Stack
data-test="popover-container"
className={`popover-container foreground ${isLoading ? "loading" : ""}`} className={`popover-container foreground ${isLoading ? "loading" : ""}`}
tokens={{ childrenGap: 20 }} tokens={{ childrenGap: 20 }}
style={{ maxWidth: 450 }} style={{ maxWidth: 450 }}
> >
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} /> <LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
<Text variant="mediumPlus" style={{ fontWeight: 600 }}> <Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}>
{title} {title}
</Text> </Text>
<Text>{children}</Text> <Text className="themeText">{children}</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}> <Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} /> <PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} /> <DefaultButton text="No" onClick={onCancel} disabled={isLoading} />

View File

@@ -4,14 +4,15 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
<div> <div>
<div <div
class="ms-Stack popover-container foreground css-109" class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<span <span
class="css-110" class="themeText css-110"
style="font-weight: 600;" style="font-weight: 600;"
/> />
<span <span
class="css-111" class="themeText css-111"
> >
<div> <div>
Test content Test content
@@ -71,10 +72,11 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
<div> <div>
<div <div
class="ms-Stack popover-container foreground css-109" class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<span <span
class="css-110" class="themeText css-110"
style="font-weight: 600;" style="font-weight: 600;"
> >
Test Title Test Title
@@ -133,10 +135,11 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
<div> <div>
<div <div
class="ms-Stack popover-container foreground css-109" class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<span <span
class="css-110" class="themeText css-110"
style="font-weight: 600;" style="font-weight: 600;"
> >
Test Title Test Title
@@ -195,16 +198,17 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
<div> <div>
<div <div
class="ms-Stack popover-container foreground css-109" class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<span <span
class="css-110" class="themeText css-110"
style="font-weight: 600;" style="font-weight: 600;"
> >
This is a very long title that might cause layout issues or text wrapping in the popover component This is a very long title that might cause layout issues or text wrapping in the popover component
</span> </span>
<span <span
class="css-111" class="themeText css-111"
> >
<div> <div>
Test content Test content
@@ -266,16 +270,17 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
<div> <div>
<div <div
class="ms-Stack popover-container foreground css-109" class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<span <span
class="css-110" class="themeText css-110"
style="font-weight: 600;" style="font-weight: 600;"
> >
Test Title Test Title
</span> </span>
<span <span
class="css-111" class="themeText css-111"
> >
<div> <div>
Test content Test content
@@ -335,16 +340,17 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
<div> <div>
<div <div
class="ms-Stack popover-container foreground css-109" class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<span <span
class="css-110" class="themeText css-110"
style="font-weight: 600;" style="font-weight: 600;"
> >
Test Title Test Title
</span> </span>
<span <span
class="css-111" class="themeText css-111"
> >
<div> <div>
<p> <p>
@@ -409,16 +415,17 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
<div> <div>
<div <div
class="ms-Stack popover-container foreground css-109" class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<span <span
class="css-110" class="themeText css-110"
style="font-weight: 600;" style="font-weight: 600;"
> >
Custom Title Custom Title
</span> </span>
<span <span
class="css-111" class="themeText css-111"
> >
<div> <div>
Test content Test content
@@ -478,6 +485,7 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
<div> <div>
<div <div
class="ms-Stack popover-container foreground loading css-109" class="ms-Stack popover-container foreground loading css-109"
data-test="popover-container"
style="max-width: 450px;" style="max-width: 450px;"
> >
<div <div
@@ -485,13 +493,13 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
data-testid="loading-overlay" data-testid="loading-overlay"
/> />
<span <span
class="css-110" class="themeText css-110"
style="font-weight: 600;" style="font-weight: 600;"
> >
Test Title Test Title
</span> </span>
<span <span
class="css-111" class="themeText css-111"
> >
<div> <div>
Test content Test content

View File

@@ -41,7 +41,7 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
return ( return (
<Stack className="addCollectionPanelWrapper"> <Stack className="addCollectionPanelWrapper">
<Stack.Item className="addCollectionPanelHeader"> <Stack.Item className="addCollectionPanelHeader">
<Text>{ContainerCopyMessages.createNewContainerSubHeading}</Text> <Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
</Stack.Item> </Stack.Item>
<Stack.Item className="addCollectionPanelBody"> <Stack.Item className="addCollectionPanelBody">
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} /> <AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />

View File

@@ -9,7 +9,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
class="ms-StackItem addCollectionPanelHeader css-110" class="ms-StackItem addCollectionPanelHeader css-110"
> >
<span <span
class="css-111" class="themeText css-111"
> >
Select the properties for your container. Select the properties for your container.
</span> </span>
@@ -50,7 +50,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
class="ms-StackItem addCollectionPanelHeader css-110" class="ms-StackItem addCollectionPanelHeader css-110"
> >
<span <span
class="css-111" class="themeText css-111"
> >
Select the properties for your container. Select the properties for your container.
</span> </span>
@@ -91,7 +91,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
class="ms-StackItem addCollectionPanelHeader css-110" class="ms-StackItem addCollectionPanelHeader css-110"
> >
<span <span
class="css-111" class="themeText css-111"
> >
Select the properties for your container. Select the properties for your container.
</span> </span>
@@ -132,7 +132,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
class="ms-StackItem addCollectionPanelHeader css-110" class="ms-StackItem addCollectionPanelHeader css-110"
> >
<span <span
class="css-111" class="themeText css-111"
> >
Select the properties for your container. Select the properties for your container.
</span> </span>

View File

@@ -22,6 +22,7 @@ const CreateCopyJobScreens: React.FC = () => {
<Stack.Item className="createCopyJobScreensContent"> <Stack.Item className="createCopyJobScreensContent">
{contextError && ( {contextError && (
<MessageBar <MessageBar
data-test="Panel:ErrorContainer"
className="createCopyJobErrorMessageBar" className="createCopyJobErrorMessageBar"
messageBarType={MessageBarType.blocked} messageBarType={MessageBarType.blocked}
isMultiline={false} isMultiline={false}

View File

@@ -31,17 +31,21 @@ const PreviewCopyJob: React.FC = () => {
})); }));
}; };
return ( return (
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer"> <Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
<FieldRow label={ContainerCopyMessages.jobNameLabel}> <FieldRow label={ContainerCopyMessages.jobNameLabel}>
<TextField value={jobName} onChange={onJobNameChange} /> <TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
</FieldRow> </FieldRow>
<Stack> <Stack>
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text> <Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
<Text>{copyJobState.source?.subscription?.displayName}</Text> <Text data-test="source-subscription-name" className="themeText">
{copyJobState.source?.subscription?.displayName}
</Text>
</Stack> </Stack>
<Stack> <Stack>
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text> <Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text>{copyJobState.source?.account?.name}</Text> <Text data-test="source-account-name" className="themeText">
{copyJobState.source?.account?.name}
</Text>
</Stack> </Stack>
<Stack> <Stack>
<DetailsList <DetailsList

View File

@@ -3,6 +3,7 @@
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = ` exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -32,6 +33,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField84" id="TextField84"
type="text" type="text"
value="job-with@special#chars_123" value="job-with@special#chars_123"
@@ -45,12 +47,13 @@ exports[`PreviewCopyJob should handle special characters in database and contain
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -59,12 +62,13 @@ exports[`PreviewCopyJob should handle special characters in database and contain
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-account-name"
> >
test-account test-account
</span> </span>
@@ -321,6 +325,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = ` exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -350,6 +355,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField96" id="TextField96"
type="text" type="text"
value="" value=""
@@ -363,12 +369,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -377,12 +384,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-account-name"
> >
test-account test-account
</span> </span>
@@ -639,6 +647,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
exports[`PreviewCopyJob should render with default state and empty job name 1`] = ` exports[`PreviewCopyJob should render with default state and empty job name 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -668,6 +677,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField0" id="TextField0"
type="text" type="text"
value="" value=""
@@ -681,12 +691,13 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -695,12 +706,13 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-account-name"
> >
test-account test-account
</span> </span>
@@ -957,6 +969,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
exports[`PreviewCopyJob should render with long subscription and account names 1`] = ` exports[`PreviewCopyJob should render with long subscription and account names 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -986,6 +999,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField60" id="TextField60"
type="text" type="text"
value="" value=""
@@ -999,12 +1013,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-subscription-name"
> >
This is a very long subscription name that might cause display issues if not handled properly This is a very long subscription name that might cause display issues if not handled properly
</span> </span>
@@ -1013,12 +1028,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-account-name"
> >
this-is-a-very-long-database-account-name-that-might-cause-display-issues this-is-a-very-long-database-account-name-that-might-cause-display-issues
</span> </span>
@@ -1275,6 +1291,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
exports[`PreviewCopyJob should render with missing source account information 1`] = ` exports[`PreviewCopyJob should render with missing source account information 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -1304,6 +1321,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField36" id="TextField36"
type="text" type="text"
value="" value=""
@@ -1317,12 +1335,13 @@ exports[`PreviewCopyJob should render with missing source account information 1`
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -1331,7 +1350,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
@@ -1588,6 +1607,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
exports[`PreviewCopyJob should render with missing source subscription information 1`] = ` exports[`PreviewCopyJob should render with missing source subscription information 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -1617,6 +1637,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField24" id="TextField24"
type="text" type="text"
value="" value=""
@@ -1630,7 +1651,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
@@ -1639,12 +1660,13 @@ exports[`PreviewCopyJob should render with missing source subscription informati
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-account-name"
> >
test-account test-account
</span> </span>
@@ -1901,6 +1923,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
exports[`PreviewCopyJob should render with online migration type 1`] = ` exports[`PreviewCopyJob should render with online migration type 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -1930,6 +1953,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField72" id="TextField72"
type="text" type="text"
value="online-migration-job" value="online-migration-job"
@@ -1943,12 +1967,13 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -1957,12 +1982,13 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-account-name"
> >
test-account test-account
</span> </span>
@@ -2219,6 +2245,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
exports[`PreviewCopyJob should render with pre-filled job name 1`] = ` exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -2248,6 +2275,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField12" id="TextField12"
type="text" type="text"
value="custom-job-name-123" value="custom-job-name-123"
@@ -2261,12 +2289,13 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -2275,12 +2304,13 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-account-name"
> >
test-account test-account
</span> </span>
@@ -2537,6 +2567,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
exports[`PreviewCopyJob should render with undefined database and container names 1`] = ` exports[`PreviewCopyJob should render with undefined database and container names 1`] = `
<div <div
class="ms-Stack previewCopyJobContainer css-109" class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
> >
<div <div
class="ms-Stack flex-row css-110" class="ms-Stack flex-row css-110"
@@ -2566,6 +2597,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
<input <input
aria-invalid="false" aria-invalid="false"
class="ms-TextField-field field-115" class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField48" id="TextField48"
type="text" type="text"
value="" value=""
@@ -2579,12 +2611,13 @@ exports[`PreviewCopyJob should render with undefined database and container name
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source subscription Source subscription
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -2593,12 +2626,13 @@ exports[`PreviewCopyJob should render with undefined database and container name
class="ms-Stack css-124" class="ms-Stack css-124"
> >
<span <span
class="bold css-125" class="bold themeText css-125"
> >
Source account Source account
</span> </span>
<span <span
class="css-125" class="themeText css-125"
data-test="source-account-name"
> >
test-account test-account
</span> </span>

View File

@@ -9,7 +9,7 @@ import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes"; import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { AccountDropdown } from "./AccountDropdown"; import { AccountDropdown, normalizeAccountId } from "./AccountDropdown";
jest.mock("../../../../../../hooks/useDatabaseAccounts"); jest.mock("../../../../../../hooks/useDatabaseAccounts");
jest.mock("../../../../../../UserContext", () => ({ jest.mock("../../../../../../UserContext", () => ({
@@ -202,13 +202,16 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0]; const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState); const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toBe(mockDatabaseAccount1); expect(newState.source.account).toEqual({
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
});
}); });
it("should auto-select predefined account from userContext if available", async () => { it("should auto-select predefined account from userContext if available", async () => {
const userContextAccount = { const userContextAccount = {
...mockDatabaseAccount2, ...mockDatabaseAccount2,
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2", id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account2",
}; };
(userContext as any).databaseAccount = userContextAccount; (userContext as any).databaseAccount = userContextAccount;
@@ -223,7 +226,10 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0]; const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState); const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toBe(mockDatabaseAccount2); expect(newState.source.account).toEqual({
...mockDatabaseAccount2,
id: normalizeAccountId(mockDatabaseAccount2.id),
});
}); });
it("should keep current account if it exists in the filtered list", async () => { it("should keep current account if it exists in the filtered list", async () => {
@@ -248,7 +254,16 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0]; const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState); const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
expect(newState).toBe(contextWithSelectedAccount.copyJobState); expect(newState).toEqual({
...contextWithSelectedAccount.copyJobState,
source: {
...contextWithSelectedAccount.copyJobState.source,
account: {
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
},
},
});
}); });
it("should handle account change when user selects different account", async () => { it("should handle account change when user selects different account", async () => {
@@ -272,7 +287,7 @@ describe("AccountDropdown", () => {
it("should normalize account ID for Portal platform", () => { it("should normalize account ID for Portal platform", () => {
const portalAccount = { const portalAccount = {
...mockDatabaseAccount1, ...mockDatabaseAccount1,
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account1", id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account1",
}; };
(configContext as any).platform = Platform.Portal; (configContext as any).platform = Platform.Portal;

View File

@@ -12,7 +12,7 @@ import FieldRow from "../../Components/FieldRow";
interface AccountDropdownProps {} interface AccountDropdownProps {}
const normalizeAccountId = (id: string) => { export const normalizeAccountId = (id: string = "") => {
if (configContext.platform === Platform.Portal) { if (configContext.platform === Platform.Portal) {
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/"); return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
} else if (configContext.platform === Platform.Hosted) { } else if (configContext.platform === Platform.Hosted) {
@@ -27,7 +27,12 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId; const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId); const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
const sqlApiOnlyAccounts: DatabaseAccount[] = (allAccounts || []).filter((account) => apiType(account) === "SQL"); const sqlApiOnlyAccounts = (allAccounts || [])
.filter((account) => apiType(account) === "SQL")
.map((account) => ({
...account,
id: normalizeAccountId(account.id),
}));
const updateCopyJobState = (newAccount: DatabaseAccount) => { const updateCopyJobState = (newAccount: DatabaseAccount) => {
setCopyJobState((prevState) => { setCopyJobState((prevState) => {
@@ -47,7 +52,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
useEffect(() => { useEffect(() => {
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) { if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
const currentAccountId = copyJobState?.source?.account?.id; const currentAccountId = copyJobState?.source?.account?.id;
const predefinedAccountId = userContext.databaseAccount?.id; const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
const selectedAccountId = currentAccountId || predefinedAccountId; const selectedAccountId = currentAccountId || predefinedAccountId;
const targetAccount: DatabaseAccount | null = const targetAccount: DatabaseAccount | null =
@@ -58,7 +63,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const accountOptions = const accountOptions =
sqlApiOnlyAccounts?.map((account) => ({ sqlApiOnlyAccounts?.map((account) => ({
key: normalizeAccountId(account.id), key: account.id,
text: account.name, text: account.name,
data: account, data: account,
})) || []; })) || [];

View File

@@ -1,6 +1,6 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { Checkbox, Stack } from "@fluentui/react"; import { Checkbox, ICheckboxStyles, Stack } from "@fluentui/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import ContainerCopyMessages from "../../../../ContainerCopyMessages";
@@ -9,8 +9,25 @@ interface MigrationTypeCheckboxProps {
onChange: (_ev?: React.FormEvent, checked?: boolean) => void; onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
} }
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => ( const checkboxStyles: ICheckboxStyles = {
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow"> text: { color: "var(--colorNeutralForeground1)" },
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} /> checkbox: { borderColor: "var(--colorNeutralStroke1)" },
root: {
selectors: {
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" },
},
},
};
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => {
return (
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow" data-test="migration-type-checkbox">
<Checkbox
label={ContainerCopyMessages.migrationTypeCheckboxLabel}
checked={checked}
onChange={onChange}
styles={checkboxStyles}
/>
</Stack> </Stack>
)); );
});

View File

@@ -3,6 +3,7 @@
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = ` exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
<div <div
class="ms-Stack migrationTypeRow css-109" class="ms-Stack migrationTypeRow css-109"
data-test="migration-type-checkbox"
> >
<div <div
class="ms-Checkbox is-checked is-enabled root-119" class="ms-Checkbox is-checked is-enabled root-119"
@@ -43,6 +44,7 @@ exports[`MigrationTypeCheckbox Component Rendering should render in checked stat
exports[`MigrationTypeCheckbox Component Rendering should render with default props (unchecked state) 1`] = ` exports[`MigrationTypeCheckbox Component Rendering should render with default props (unchecked state) 1`] = `
<div <div
class="ms-Stack migrationTypeRow css-109" class="ms-Stack migrationTypeRow css-109"
data-test="migration-type-checkbox"
> >
<div <div
class="ms-Checkbox is-enabled root-110" class="ms-Checkbox is-enabled root-110"

View File

@@ -21,7 +21,7 @@ const SelectAccount = React.memo(() => {
return ( return (
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}> <Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
<Text>{ContainerCopyMessages.selectAccountDescription}</Text> <Text className="themeText">{ContainerCopyMessages.selectAccountDescription}</Text>
<SubscriptionDropdown /> <SubscriptionDropdown />

View File

@@ -6,7 +6,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
data-test="Panel:SelectAccountContainer" data-test="Panel:SelectAccountContainer"
> >
<span <span
class="css-110" class="themeText css-110"
> >
Please select a source account from which to copy. Please select a source account from which to copy.
</span> </span>

View File

@@ -47,8 +47,12 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
const onDropdownChange = dropDownChangeHandler(setCopyJobState); const onDropdownChange = dropDownChangeHandler(setCopyJobState);
return ( return (
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}> <Stack
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span> data-test="Panel:SelectSourceAndTargetContainers"
className="selectSourceAndTargetContainers"
tokens={{ childrenGap: 25 }}
>
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
<DatabaseContainerSection <DatabaseContainerSection
heading={ContainerCopyMessages.sourceContainerSubHeading} heading={ContainerCopyMessages.sourceContainerSubHeading}
databaseOptions={sourceDatabaseOptions} databaseOptions={sourceDatabaseOptions}
@@ -59,6 +63,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
selectedContainer={source?.containerId} selectedContainer={source?.containerId}
containerDisabled={!source?.databaseId} containerDisabled={!source?.databaseId}
containerOnChange={onDropdownChange("sourceContainer")} containerOnChange={onDropdownChange("sourceContainer")}
sectionType="source"
/> />
<DatabaseContainerSection <DatabaseContainerSection
heading={ContainerCopyMessages.targetContainerSubHeading} heading={ContainerCopyMessages.targetContainerSubHeading}
@@ -71,6 +76,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
containerDisabled={!target?.databaseId} containerDisabled={!target?.databaseId}
containerOnChange={onDropdownChange("targetContainer")} containerOnChange={onDropdownChange("targetContainer")}
handleOnDemandCreateContainer={showAddCollectionPanel} handleOnDemandCreateContainer={showAddCollectionPanel}
sectionType="target"
/> />
</Stack> </Stack>
); );

View File

@@ -32,6 +32,7 @@ describe("DatabaseContainerSection", () => {
selectedContainer: "container1", selectedContainer: "container1",
containerDisabled: false, containerDisabled: false,
containerOnChange: mockContainerOnChange, containerOnChange: mockContainerOnChange,
sectionType: "source",
}; };
beforeEach(() => { beforeEach(() => {
@@ -292,6 +293,7 @@ describe("DatabaseContainerSection", () => {
containerOptions: mockContainerOptions, containerOptions: mockContainerOptions,
selectedContainer: "container1", selectedContainer: "container1",
containerOnChange: mockContainerOnChange, containerOnChange: mockContainerOnChange,
sectionType: "source",
}; };
render(<DatabaseContainerSection {...minimalProps} />); render(<DatabaseContainerSection {...minimalProps} />);
@@ -393,6 +395,7 @@ describe("DatabaseContainerSection", () => {
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }], containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
selectedContainer: "c1", selectedContainer: "c1",
containerOnChange: jest.fn(), containerOnChange: jest.fn(),
sectionType: "source",
}; };
const { container } = render(<DatabaseContainerSection {...minimalProps} />); const { container } = render(<DatabaseContainerSection {...minimalProps} />);
@@ -411,6 +414,7 @@ describe("DatabaseContainerSection", () => {
containerDisabled: false, containerDisabled: false,
containerOnChange: jest.fn(), containerOnChange: jest.fn(),
handleOnDemandCreateContainer: jest.fn(), handleOnDemandCreateContainer: jest.fn(),
sectionType: "target",
}; };
const { container } = render(<DatabaseContainerSection {...fullProps} />); const { container } = render(<DatabaseContainerSection {...fullProps} />);
@@ -428,6 +432,7 @@ describe("DatabaseContainerSection", () => {
selectedContainer: "container1", selectedContainer: "container1",
containerDisabled: true, containerDisabled: true,
containerOnChange: jest.fn(), containerOnChange: jest.fn(),
sectionType: "target",
}; };
const { container } = render(<DatabaseContainerSection {...disabledProps} />); const { container } = render(<DatabaseContainerSection {...disabledProps} />);
@@ -443,6 +448,7 @@ describe("DatabaseContainerSection", () => {
containerOptions: [], containerOptions: [],
selectedContainer: "", selectedContainer: "",
containerOnChange: jest.fn(), containerOnChange: jest.fn(),
sectionType: "target",
}; };
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />); const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);

View File

@@ -15,6 +15,7 @@ export const DatabaseContainerSection = ({
containerDisabled, containerDisabled,
containerOnChange, containerOnChange,
handleOnDemandCreateContainer, handleOnDemandCreateContainer,
sectionType,
}: DatabaseContainerSectionProps) => ( }: DatabaseContainerSectionProps) => (
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection"> <Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
<label className="subHeading">{heading}</label> <label className="subHeading">{heading}</label>
@@ -27,6 +28,7 @@ export const DatabaseContainerSection = ({
disabled={!!databaseDisabled} disabled={!!databaseDisabled}
selectedKey={selectedDatabase} selectedKey={selectedDatabase}
onChange={databaseOnChange} onChange={databaseOnChange}
data-test={`${sectionType}-databaseDropdown`}
/> />
</FieldRow> </FieldRow>
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}> <FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
@@ -39,9 +41,14 @@ export const DatabaseContainerSection = ({
disabled={!!containerDisabled} disabled={!!containerDisabled}
selectedKey={selectedContainer} selectedKey={selectedContainer}
onChange={containerOnChange} onChange={containerOnChange}
data-test={`${sectionType}-containerDropdown`}
/> />
{handleOnDemandCreateContainer && ( {handleOnDemandCreateContainer && (
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}> <ActionButton
className="create-container-link-btn"
style={{ color: "var(--colorBrandForeground1)" }}
onClick={() => handleOnDemandCreateContainer()}
>
{ContainerCopyMessages.createContainerButtonLabel} {ContainerCopyMessages.createContainerButtonLabel}
</ActionButton> </ActionButton>
)} )}

View File

@@ -37,6 +37,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
class="ms-Dropdown is-required dropdown-112" class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown98" id="Dropdown98"
role="combobox" role="combobox"
tabindex="0" tabindex="0"
@@ -94,6 +95,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
class="ms-Dropdown is-required dropdown-112" class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown99" id="Dropdown99"
role="combobox" role="combobox"
tabindex="0" tabindex="0"
@@ -182,6 +184,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
class="ms-Dropdown is-disabled is-required dropdown-143" class="ms-Dropdown is-disabled is-required dropdown-143"
data-is-focusable="false" data-is-focusable="false"
data-ktp-target="true" data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown103" id="Dropdown103"
role="combobox" role="combobox"
tabindex="-1" tabindex="-1"
@@ -239,6 +242,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
class="ms-Dropdown is-disabled is-required dropdown-143" class="ms-Dropdown is-disabled is-required dropdown-143"
data-is-focusable="false" data-is-focusable="false"
data-ktp-target="true" data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown104" id="Dropdown104"
role="combobox" role="combobox"
tabindex="-1" tabindex="-1"
@@ -306,6 +310,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
class="ms-Dropdown is-required dropdown-112" class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown105" id="Dropdown105"
role="combobox" role="combobox"
tabindex="0" tabindex="0"
@@ -363,6 +368,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
class="ms-Dropdown is-required dropdown-112" class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown106" id="Dropdown106"
role="combobox" role="combobox"
tabindex="0" tabindex="0"
@@ -430,6 +436,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
class="ms-Dropdown is-required dropdown-112" class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="source-databaseDropdown"
id="Dropdown96" id="Dropdown96"
role="combobox" role="combobox"
tabindex="0" tabindex="0"
@@ -487,6 +494,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
class="ms-Dropdown is-required dropdown-112" class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true" data-is-focusable="true"
data-ktp-target="true" data-ktp-target="true"
data-test="source-containerDropdown"
id="Dropdown97" id="Dropdown97"
role="combobox" role="combobox"
tabindex="0" tabindex="0"

View File

@@ -83,6 +83,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
return ( return (
<IconButton <IconButton
data-test={`CopyJobActionMenu/Button:${job.Name}`}
role="button" role="button"
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }} iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
menuProps={{ items: getMenuItems() }} menuProps={{ items: getMenuItems() }}

View File

@@ -1,5 +1,6 @@
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react"; import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
import React, { memo } from "react"; import React, { memo } from "react";
import { useThemeStore } from "../../../../hooks/useTheme";
import ContainerCopyMessages from "../../ContainerCopyMessages"; import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType } from "../../Types/CopyJobTypes"; import { CopyJobType } from "../../Types/CopyJobTypes";
@@ -63,6 +64,19 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
}; };
const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => { const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
const errorMessageStyle: React.CSSProperties = {
whiteSpace: "pre-wrap",
...(isDarkMode && {
whiteSpace: "pre-wrap",
backgroundColor: "var(--colorNeutralBackground2)",
color: "var(--colorNeutralForeground1)",
padding: "10px",
borderRadius: "4px",
}),
};
const selectedContainers = [ const selectedContainers = [
{ {
sourceContainerName: job?.Source?.containerName || "N/A", sourceContainerName: job?.Source?.containerName || "N/A",
@@ -77,10 +91,10 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
<Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details"> <Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details">
{job.Error ? ( {job.Error ? (
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}> <Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
<Text className="bold" style={sectionCss.headingText}> <Text className="bold themeText" style={sectionCss.headingText}>
{ContainerCopyMessages.errorTitle} {ContainerCopyMessages.errorTitle}
</Text> </Text>
<Text as="pre" style={{ whiteSpace: "pre-wrap" }}> <Text as="pre" style={errorMessageStyle}>
{job.Error.message} {job.Error.message}
</Text> </Text>
</Stack.Item> </Stack.Item>
@@ -88,16 +102,16 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
<Stack.Item data-testid="selectedcollection-stack"> <Stack.Item data-testid="selectedcollection-stack">
<Stack tokens={{ childrenGap: 15 }}> <Stack tokens={{ childrenGap: 15 }}>
<Stack.Item style={sectionCss.verticalAlign}> <Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text> <Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
<Text>{job.LastUpdatedTime}</Text> <Text className="themeText">{job.LastUpdatedTime}</Text>
</Stack.Item> </Stack.Item>
<Stack.Item style={sectionCss.verticalAlign}> <Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text> <Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text>{job.Source?.remoteAccountName}</Text> <Text className="themeText">{job.Source?.remoteAccountName}</Text>
</Stack.Item> </Stack.Item>
<Stack.Item style={sectionCss.verticalAlign}> <Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text> <Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
<Text>{job.Mode}</Text> <Text className="themeText">{job.Mode}</Text>
</Stack.Item> </Stack.Item>
</Stack> </Stack>
</Stack.Item> </Stack.Item>

View File

@@ -1,30 +1,14 @@
import { FontIcon, getTheme, mergeStyles, mergeStyleSets, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react"; import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages"; import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
const theme = getTheme();
const iconClass = mergeStyles({ const iconClass = mergeStyles({
fontSize: "16px", fontSize: "16px",
marginRight: "8px", marginRight: "8px",
}); });
const classNames = mergeStyleSets({
[CopyJobStatusType.Pending]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
[CopyJobStatusType.InProgress]: [{ color: theme.palette.themePrimary }, iconClass],
[CopyJobStatusType.Running]: [{ color: theme.palette.themePrimary }, iconClass],
[CopyJobStatusType.Partitioning]: [{ color: theme.palette.themePrimary }, iconClass],
[CopyJobStatusType.Paused]: [{ color: theme.palette.themePrimary }, iconClass],
[CopyJobStatusType.Skipped]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
[CopyJobStatusType.Cancelled]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
[CopyJobStatusType.Failed]: [{ color: theme.semanticColors.errorIcon }, iconClass],
[CopyJobStatusType.Faulted]: [{ color: theme.semanticColors.errorIcon }, iconClass],
[CopyJobStatusType.Completed]: [{ color: theme.semanticColors.successIcon }, iconClass],
unknown: [{ color: theme.semanticColors.bodySubtext }, iconClass],
});
const iconMap: Partial<Record<CopyJobStatusType, string>> = { const iconMap: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Pending]: "Clock", [CopyJobStatusType.Pending]: "Clock",
[CopyJobStatusType.Paused]: "CirclePause", [CopyJobStatusType.Paused]: "CirclePause",
@@ -35,6 +19,17 @@ const iconMap: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Completed]: "CompletedSolid", [CopyJobStatusType.Completed]: "CompletedSolid",
}; };
// Icon colors for different statuses
const statusIconColors: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Failed]: "var(--colorPaletteRedForeground1)",
[CopyJobStatusType.Faulted]: "var(--colorPaletteRedForeground1)",
[CopyJobStatusType.Completed]: "var(--colorSuccessGreen)",
[CopyJobStatusType.InProgress]: "var(--colorBrandForeground1)",
[CopyJobStatusType.Running]: "var(--colorBrandForeground1)",
[CopyJobStatusType.Partitioning]: "var(--colorBrandForeground1)",
[CopyJobStatusType.Paused]: "var(--colorBrandForeground1)",
};
export interface CopyJobStatusWithIconProps { export interface CopyJobStatusWithIconProps {
status: CopyJobStatusType; status: CopyJobStatusType;
} }
@@ -47,19 +42,17 @@ const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo((
CopyJobStatusType.InProgress, CopyJobStatusType.InProgress,
CopyJobStatusType.Partitioning, CopyJobStatusType.Partitioning,
].includes(status); ].includes(status);
const iconColor = statusIconColors[status] || "var(--colorNeutralForeground2)";
const iconStyle = mergeStyles(iconClass, { color: iconColor });
return ( return (
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
{isSpinnerStatus ? ( {isSpinnerStatus ? (
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} /> <Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
) : ( ) : (
<FontIcon <FontIcon aria-label={status} iconName={iconMap[status] || "UnknownSolid"} className={iconStyle} />
aria-label={status}
iconName={iconMap[status] || "UnknownSolid"}
className={classNames[status] || classNames.unknown}
/>
)} )}
<Text>{statusText}</Text> <Text className="themeText">{statusText}</Text>
</Stack> </Stack>
); );
}); });

View File

@@ -15,6 +15,8 @@ import {
} from "@fluentui/react"; } from "@fluentui/react";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import Pager from "../../../../Common/Pager"; import Pager from "../../../../Common/Pager";
import { useThemeStore } from "../../../../hooks/useTheme";
import { getThemeTokens } from "../../../Theme/ThemeUtil";
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions"; import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
import { getColumns } from "./CopyJobColumns"; import { getColumns } from "./CopyJobColumns";
@@ -26,13 +28,15 @@ interface CopyJobsListProps {
} }
const styles = { const styles = {
container: { height: "calc(100vh - 25em)" } as React.CSSProperties, container: { height: "100%" } as React.CSSProperties,
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties, stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
}; };
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => { const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
const themeTokens = getThemeTokens(isDarkMode);
const [startIndex, setStartIndex] = React.useState(0); const [startIndex, setStartIndex] = React.useState(0);
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs); const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined); const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
@@ -80,6 +84,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}> <Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}> <ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<ShimmeredDetailsList <ShimmeredDetailsList
className="CopyJobListContainer"
onRenderRow={_onRenderRow} onRenderRow={_onRenderRow}
checkboxVisibility={2} checkboxVisibility={2}
columns={columns} columns={columns}
@@ -87,11 +92,28 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
enableShimmer={false} enableShimmer={false}
constrainMode={ConstrainMode.unconstrained} constrainMode={ConstrainMode.unconstrained}
layoutMode={DetailsListLayoutMode.justified} layoutMode={DetailsListLayoutMode.justified}
onRenderDetailsHeader={(props, defaultRender) => ( onRenderDetailsHeader={(props, defaultRender) => {
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced> const bgColor = themeTokens.colorNeutralBackground3;
{defaultRender({ ...props })} const textColor = themeTokens.colorNeutralForeground1;
return (
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced stickyBackgroundColor={bgColor}>
<div style={{ backgroundColor: bgColor }}>
{defaultRender({
...props,
styles: {
root: {
backgroundColor: bgColor,
selectors: {
".ms-DetailsHeader-cellTitle": { color: textColor },
".ms-DetailsHeader-cellName": { color: textColor },
},
},
},
})}
</div>
</Sticky> </Sticky>
)} );
}}
/> />
</ScrollablePane> </ScrollablePane>
</Stack.Item> </Stack.Item>

View File

@@ -13,7 +13,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spin
/> />
</div> </div>
<span <span
class="css-112" class="themeText css-112"
> >
Running Running
</span> </span>
@@ -33,7 +33,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with sp
/> />
</div> </div>
<span <span
class="css-112" class="themeText css-112"
> >
Running Running
</span> </span>
@@ -53,7 +53,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner
/> />
</div> </div>
<span <span
class="css-112" class="themeText css-112"
> >
Running Running
</span> </span>
@@ -66,7 +66,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
> >
<i <i
aria-label="Cancelled" aria-label="Cancelled"
class="ms-Icon root-105 css-118 mocked-style-Cancelled" class="ms-Icon root-105 css-118 mocked-styles"
data-icon-name="StatusErrorFull" data-icon-name="StatusErrorFull"
role="img" role="img"
style="font-family: "FabricMDL2Icons-4";" style="font-family: "FabricMDL2Icons-4";"
@@ -74,7 +74,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i> </i>
<span <span
class="css-112" class="themeText css-112"
> >
Cancelled Cancelled
</span> </span>
@@ -87,7 +87,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
> >
<i <i
aria-label="Completed" aria-label="Completed"
class="ms-Icon root-105 css-120 mocked-style-Completed" class="ms-Icon root-105 css-120 mocked-styles"
data-icon-name="CompletedSolid" data-icon-name="CompletedSolid"
role="img" role="img"
style="font-family: "FabricMDL2Icons-5";" style="font-family: "FabricMDL2Icons-5";"
@@ -95,7 +95,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i> </i>
<span <span
class="css-112" class="themeText css-112"
> >
Completed Completed
</span> </span>
@@ -108,7 +108,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
> >
<i <i
aria-label="Failed" aria-label="Failed"
class="ms-Icon root-105 css-118 mocked-style-Failed" class="ms-Icon root-105 css-118 mocked-styles"
data-icon-name="StatusErrorFull" data-icon-name="StatusErrorFull"
role="img" role="img"
style="font-family: "FabricMDL2Icons-4";" style="font-family: "FabricMDL2Icons-4";"
@@ -116,7 +116,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i> </i>
<span <span
class="css-112" class="themeText css-112"
> >
Failed Failed
</span> </span>
@@ -129,7 +129,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
> >
<i <i
aria-label="Faulted" aria-label="Faulted"
class="ms-Icon root-105 css-118 mocked-style-Faulted" class="ms-Icon root-105 css-118 mocked-styles"
data-icon-name="StatusErrorFull" data-icon-name="StatusErrorFull"
role="img" role="img"
style="font-family: "FabricMDL2Icons-4";" style="font-family: "FabricMDL2Icons-4";"
@@ -137,7 +137,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i> </i>
<span <span
class="css-112" class="themeText css-112"
> >
Failed Failed
</span> </span>
@@ -150,7 +150,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
> >
<i <i
aria-label="Paused" aria-label="Paused"
class="ms-Icon root-105 css-114 mocked-style-Paused" class="ms-Icon root-105 css-114 mocked-styles"
data-icon-name="CirclePause" data-icon-name="CirclePause"
role="img" role="img"
style="font-family: "FabricMDL2Icons-11";" style="font-family: "FabricMDL2Icons-11";"
@@ -158,7 +158,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i> </i>
<span <span
class="css-112" class="themeText css-112"
> >
Paused Paused
</span> </span>
@@ -171,7 +171,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
> >
<i <i
aria-label="Pending" aria-label="Pending"
class="ms-Icon root-105 css-111 mocked-style-Pending" class="ms-Icon root-105 css-111 mocked-styles"
data-icon-name="Clock" data-icon-name="Clock"
role="img" role="img"
style="font-family: "FabricMDL2Icons-2";" style="font-family: "FabricMDL2Icons-2";"
@@ -179,7 +179,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i> </i>
<span <span
class="css-112" class="themeText css-112"
> >
Queued Queued
</span> </span>
@@ -192,7 +192,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
> >
<i <i
aria-label="Skipped" aria-label="Skipped"
class="ms-Icon root-105 css-116 mocked-style-Skipped" class="ms-Icon root-105 css-116 mocked-styles"
data-icon-name="StatusCircleBlock2" data-icon-name="StatusCircleBlock2"
role="img" role="img"
style="font-family: "FabricMDL2Icons-9";" style="font-family: "FabricMDL2Icons-9";"
@@ -200,7 +200,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i> </i>
<span <span
class="css-112" class="themeText css-112"
> >
Cancelled Cancelled
</span> </span>

View File

@@ -10,7 +10,7 @@ import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes"; import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
import CopyJobsList from "./Components/CopyJobsList"; import CopyJobsList from "./Components/CopyJobsList";
const FETCH_INTERVAL_MS = 30 * 1000; const FETCH_INTERVAL = 2 * 60 * 1000;
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" }); const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
interface MonitorCopyJobsProps { interface MonitorCopyJobsProps {
@@ -57,7 +57,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
useEffect(() => { useEffect(() => {
fetchJobs(); fetchJobs();
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS); const intervalId = setInterval(fetchJobs, FETCH_INTERVAL);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [fetchJobs]); }, [fetchJobs]);

View File

@@ -49,6 +49,7 @@ export interface DatabaseContainerSectionProps {
containerDisabled?: boolean; containerDisabled?: boolean;
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void; containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
handleOnDemandCreateContainer?: () => void; handleOnDemandCreateContainer?: () => void;
sectionType: "source" | "target";
} }
export interface CopyJobContextState { export interface CopyJobContextState {

View File

@@ -1,6 +1,30 @@
@import "../../../less/Common/Constants.less"; @import "../../../less/Common/Constants.less";
// Common theme-aware classes
.themeText {
color: var(--colorNeutralForeground1);
}
.themeTextSecondary {
color: var(--colorNeutralForeground2);
}
.themeLinkText {
color: var(--colorBrandForeground1);
}
.themeBackground {
background-color: var(--colorNeutralBackground1);
}
.themeBackgroundSecondary {
background-color: var(--colorNeutralBackground2);
}
#containerCopyWrapper { #containerCopyWrapper {
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
.centerContent { .centerContent {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -9,21 +33,31 @@
.noCopyJobsMessage { .noCopyJobsMessage {
font-weight: 600; font-weight: 600;
margin: 0 auto; margin: 0 auto;
color: @FocusColor; color: var(--colorNeutralForeground2);
} }
button.createCopyJobButton { button.createCopyJobButton {
color: @LinkColor; color: var(--colorBrandForeground1);
} }
} }
} }
.createCopyJobScreensContainer { .createCopyJobScreensContainer {
height: 100%; height: 100%;
padding: 1em 1.5em; padding: 1em 1.5em;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
.pointInTimeRestoreContainer, .onlineCopyContainer { .pointInTimeRestoreContainer, .onlineCopyContainer {
position: relative; position: relative;
} }
.toggle-label {
color: var(--colorNeutralForeground1);
}
.accordionHeaderText {
color: var(--colorNeutralForeground1);
}
label { label {
padding: 0; padding: 0;
} }
@@ -71,7 +105,7 @@
} }
.foreground { .foreground {
z-index: 10; z-index: 10;
background-color: #f9f9f9; background-color: var(--colorNeutralBackground2);
padding: 20px; padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translate(0%, -9%); transform: translate(0%, -9%);
@@ -80,14 +114,40 @@
.createCopyJobErrorMessageBar { .createCopyJobErrorMessageBar {
margin-bottom: 2em; margin-bottom: 2em;
} }
body.isDarkMode & {
.ms-TooltipHost .ms-Image {
filter: invert(1);
}
.ms-TextField {
.ms-TextField-fieldGroup {
background-color: var(--colorNeutralBackground1);
border-color: var(--colorNeutralStroke1);
}
.ms-TextField-field {
color: var(--colorNeutralForeground1);
background-color: var(--colorNeutralBackground1);
&::placeholder {
color: var(--colorNeutralForeground4);
}
}
.ms-Label {
color: var(--colorNeutralForeground1);
}
}
}
.create-container-link-btn { .create-container-link-btn {
padding: 0; padding: 0;
height: 25px; height: 25px;
color: @LinkColor; color: var(--colorBrandForeground1);
&:focus { &:focus {
outline: none; outline: none;
} }
} }
/* Create collection panel */ /* Create collection panel */
@@ -105,7 +165,6 @@
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
margin: 0 auto; margin: 0 auto;
.ms-DetailsList { .ms-DetailsList {
width: 100%; width: 100%;
@@ -114,33 +173,33 @@
padding: @DefaultSpace 20px; padding: @DefaultSpace 20px;
font-weight: 600; font-weight: 600;
font-size: @DefaultFontSize; font-size: @DefaultFontSize;
color: @BaseHigh; color: var(--colorNeutralForeground1);
background-color: @BaseLow; background-color: var(--colorNeutralBackground2);
border-bottom: @ButtonBorderWidth solid @BaseMedium; border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
&:hover { &:hover {
background-color: @BaseMediumLow; background-color: var(--colorNeutralBackground3);
} }
} }
} }
.ms-DetailsRow { .ms-DetailsRow {
border-bottom: @ButtonBorderWidth solid @BaseMedium; border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
&:hover { &:hover {
background-color: @BaseMediumLow; background-color: var(--colorNeutralBackground2);
} }
.ms-DetailsRow-cell { .ms-DetailsRow-cell {
padding: @MediumSpace 20px; padding: @MediumSpace 20px;
font-size: @DefaultFontSize; font-size: @DefaultFontSize;
color: @BaseHigh; color: var(--colorNeutralForeground1);
min-height: 48px; min-height: 48px;
display: flex; display: flex;
align-items: center; align-items: center;
.jobNameLink { .jobNameLink {
color: @LinkColor; color: var(--colorBrandForeground1);
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -168,7 +227,7 @@
} }
.ms-DetailsRow-cell { .ms-DetailsRow-cell {
font-size: @DefaultFontSize; font-size: @DefaultFontSize;
color: @BaseHigh; color: var(--colorNeutralForeground1);
} }
} }
} }

View File

@@ -1,8 +1,5 @@
import { IndexingPolicy } from "@azure/cosmos";
import { act } from "@testing-library/react";
import { AuthType } from "AuthType"; import { AuthType } from "AuthType";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import ko from "knockout"; import ko from "knockout";
import React from "react"; import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateCollection } from "../../../Common/dataAccess/updateCollection";
@@ -447,49 +444,3 @@ describe("SettingsComponent", () => {
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toBe(true); expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toBe(true);
}); });
}); });
describe("SettingsComponent - indexing policy subscription", () => {
const baseProps: SettingsComponentProps = {
settingsTab: new CollectionSettingsTabV2({
collection: collection,
tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
title: "Scale & Settings",
tabPath: "",
node: undefined,
}),
};
it("subscribes to the correct container's indexing policy and updates state on change", async () => {
const containerId = collection.id();
const mockIndexingPolicy: IndexingPolicy = {
automatic: false,
indexingMode: "lazy",
includedPaths: [{ path: "/foo/*" }],
excludedPaths: [{ path: "/bar/*" }],
compositeIndexes: [],
spatialIndexes: [],
vectorIndexes: [],
fullTextIndexes: [],
};
const wrapper = shallow(<SettingsComponent {...baseProps} />);
const instance = wrapper.instance() as SettingsComponent;
await act(async () => {
useIndexingPolicyStore.setState({
indexingPolicies: {
[containerId]: mockIndexingPolicy,
},
});
// Wait for the async refreshCollectionData to complete
await new Promise((resolve) => setTimeout(resolve, 0));
});
wrapper.update();
expect(wrapper.state("indexingPolicyContent")).toEqual(mockIndexingPolicy);
expect(wrapper.state("indexingPolicyContentBaseline")).toEqual(mockIndexingPolicy);
// @ts-expect-error: rawDataModel is intentionally accessed for test validation
expect(instance.collection.rawDataModel.indexingPolicy).toEqual(mockIndexingPolicy);
});
});

View File

@@ -13,7 +13,6 @@ import {
ThroughputBucketsComponent, ThroughputBucketsComponent,
ThroughputBucketsComponentProps, ThroughputBucketsComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent"; } from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
@@ -74,6 +73,7 @@ import {
parseConflictResolutionMode, parseConflictResolutionMode,
parseConflictResolutionProcedure, parseConflictResolutionProcedure,
} from "./SettingsUtils"; } from "./SettingsUtils";
interface SettingsV2TabInfo { interface SettingsV2TabInfo {
tab: SettingsV2TabTypes; tab: SettingsV2TabTypes;
content: JSX.Element; content: JSX.Element;
@@ -182,7 +182,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private totalThroughputUsed: number; private totalThroughputUsed: number;
private throughputBucketsEnabled: boolean; private throughputBucketsEnabled: boolean;
public mongoDBCollectionResource: MongoDBCollectionResource; public mongoDBCollectionResource: MongoDBCollectionResource;
private unsubscribe: () => void;
constructor(props: SettingsComponentProps) { constructor(props: SettingsComponentProps) {
super(props); super(props);
@@ -312,13 +312,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (this.isCollectionSettingsTab) { if (this.isCollectionSettingsTab) {
this.refreshIndexTransformationProgress(); this.refreshIndexTransformationProgress();
this.loadMongoIndexes(); this.loadMongoIndexes();
this.unsubscribe = useIndexingPolicyStore.subscribe(
() => {
this.refreshCollectionData();
},
(state) => state.indexingPolicies[this.collection?.id()],
);
this.refreshCollectionData();
} }
this.setBaseline(); this.setBaseline();
@@ -326,11 +319,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
} }
componentWillUnmount(): void {
if (this.unsubscribe) {
this.unsubscribe();
}
}
componentDidUpdate(): void { componentDidUpdate(): void {
if (this.props.settingsTab.isActive()) { if (this.props.settingsTab.isActive()) {
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
@@ -860,6 +849,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
{ name: "name_of_property", query: "query_to_compute_property" }, { name: "name_of_property", query: "query_to_compute_property" },
] as DataModels.ComputedProperties; ] as DataModels.ComputedProperties;
} }
const throughputBuckets = this.offer?.throughputBuckets; const throughputBuckets = this.offer?.throughputBuckets;
return { return {
@@ -1019,31 +1009,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
startKey, startKey,
); );
}; };
private refreshCollectionData = async (): Promise<void> => {
const containerId = this.collection.id();
const latestIndexingPolicy = useIndexingPolicyStore.getState().indexingPolicies[containerId];
const rawPolicy = latestIndexingPolicy ?? this.collection.indexingPolicy();
const latestCollection: DataModels.IndexingPolicy = {
automatic: rawPolicy?.automatic ?? true,
indexingMode: rawPolicy?.indexingMode ?? "consistent",
includedPaths: rawPolicy?.includedPaths ?? [],
excludedPaths: rawPolicy?.excludedPaths ?? [],
compositeIndexes: rawPolicy?.compositeIndexes ?? [],
spatialIndexes: rawPolicy?.spatialIndexes ?? [],
vectorIndexes: rawPolicy?.vectorIndexes ?? [],
fullTextIndexes: rawPolicy?.fullTextIndexes ?? [],
};
this.collection.rawDataModel.indexingPolicy = latestCollection;
this.setState({
indexingPolicyContent: latestCollection,
indexingPolicyContentBaseline: latestCollection,
});
};
private saveCollectionSettings = async (startKey: number): Promise<void> => { private saveCollectionSettings = async (startKey: number): Promise<void> => {
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel }; const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
if ( if (
this.state.isSubSettingsSaveable || this.state.isSubSettingsSaveable ||
this.state.isContainerPolicyDirty || this.state.isContainerPolicyDirty ||
@@ -1283,6 +1252,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onScaleDiscardableChange: this.onScaleDiscardableChange, onScaleDiscardableChange: this.onScaleDiscardableChange,
throughputError: this.state.throughputError, throughputError: this.state.throughputError,
}; };
if (!this.isCollectionSettingsTab) { if (!this.isCollectionSettingsTab) {
return ( return (
<div className="settingsV2MainContainer"> <div className="settingsV2MainContainer">

View File

@@ -155,7 +155,12 @@ export class ComputedPropertiesComponent extends React.Component<
</Link> </Link>
&#160; about how to define computed properties and how to use them. &#160; about how to define computed properties and how to use them.
</Text> </Text>
<div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div> <div
className="settingsV2Editor"
tabIndex={0}
ref={this.computedPropertiesDiv}
data-test="computed-properties-editor"
></div>
</Stack> </Stack>
); );
} }

View File

@@ -1,10 +1,10 @@
import { MessageBar, MessageBarType } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { handleError } from "../../../../../Common/ErrorHandlingUtils"; import { MessageBar, MessageBarType } from "@fluentui/react";
import { import {
mongoIndexTransformationRefreshingMessage, mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage, renderMongoIndexTransformationRefreshMessage,
} from "../../SettingsRenderUtils"; } from "../../SettingsRenderUtils";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { isIndexTransforming } from "../../SettingsUtils"; import { isIndexTransforming } from "../../SettingsUtils";
export interface IndexingPolicyRefreshComponentProps { export interface IndexingPolicyRefreshComponentProps {

View File

@@ -187,7 +187,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text> <Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
<Text styles={textSubHeadingStyle}>Partitioning</Text> <Text styles={textSubHeadingStyle}>Partitioning</Text>
</Stack> </Stack>
<Stack tokens={{ childrenGap: 5 }}> <Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text> <Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
<Text styles={textSubHeadingStyle1}> <Text styles={textSubHeadingStyle1}>
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} {isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
@@ -199,6 +199,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
{!isReadOnly && ( {!isReadOnly && (
<> <>
<MessageBar <MessageBar
data-test="partition-key-warning"
messageBarType={MessageBarType.warning} messageBarType={MessageBarType.warning}
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }} messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={darkThemeMessageBarStyles} styles={darkThemeMessageBarStyles}
@@ -220,6 +221,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
</Text> </Text>
{configContext.platform !== Platform.Emulator && ( {configContext.platform !== Platform.Emulator && (
<PrimaryButton <PrimaryButton
data-test="change-partition-key-button"
styles={{ root: { width: "fit-content" } }} styles={{ root: { width: "fit-content" } }}
text="Change" text="Change"
onClick={startPartitionkeyChangeWorkflow} onClick={startPartitionkeyChangeWorkflow}

View File

@@ -302,8 +302,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
); );
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [ private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: GeospatialConfigType.Geography, text: "Geography" }, { key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
{ key: GeospatialConfigType.Geometry, text: "Geometry" }, { key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
]; ];
private getGeoSpatialComponent = (): JSX.Element => ( private getGeoSpatialComponent = (): JSX.Element => (

View File

@@ -103,7 +103,10 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
offText="Inactive" offText="Inactive"
checked={bucket.maxThroughputPercentage !== 100} checked={bucket.maxThroughputPercentage !== 100}
onChange={(event, checked) => onToggle(bucket.id, checked)} onChange={(event, checked) => onToggle(bucket.id, checked)}
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }} styles={{
root: { marginBottom: 0 },
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
}}
></Toggle> ></Toggle>
</Stack> </Stack>
))} ))}

View File

@@ -31,6 +31,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
</Text> </Text>
<div <div
className="settingsV2Editor" className="settingsV2Editor"
data-test="computed-properties-editor"
tabIndex={0} tabIndex={0}
/> />
</Stack> </Stack>

View File

@@ -78,6 +78,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Text> </Text>
</Stack> </Stack>
<Stack <Stack
data-test="partition-key-values"
tokens={ tokens={
{ {
"childrenGap": 5, "childrenGap": 5,
@@ -108,6 +109,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Stack> </Stack>
</Stack> </Stack>
<StyledMessageBar <StyledMessageBar
data-test="partition-key-warning"
messageBarIconProps={ messageBarIconProps={
{ {
"className": "messageBarWarningIcon", "className": "messageBarWarningIcon",
@@ -160,6 +162,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
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. 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.
</Text> </Text>
<CustomizedPrimaryButton <CustomizedPrimaryButton
data-test="change-partition-key-button"
onClick={[Function]} onClick={[Function]}
styles={ styles={
{ {
@@ -237,6 +240,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
</Text> </Text>
</Stack> </Stack>
<Stack <Stack
data-test="partition-key-values"
tokens={ tokens={
{ {
"childrenGap": 5, "childrenGap": 5,

View File

@@ -167,10 +167,12 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "geography-option",
"key": "Geography", "key": "Geography",
"text": "Geography", "text": "Geography",
}, },
{ {
"ariaLabel": "geometry-option",
"key": "Geometry", "key": "Geometry",
"text": "Geometry", "text": "Geometry",
}, },
@@ -652,10 +654,12 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "geography-option",
"key": "Geography", "key": "Geography",
"text": "Geography", "text": "Geography",
}, },
{ {
"ariaLabel": "geometry-option",
"key": "Geometry", "key": "Geometry",
"text": "Geometry", "text": "Geometry",
}, },
@@ -1224,10 +1228,12 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "geography-option",
"key": "Geography", "key": "Geography",
"text": "Geography", "text": "Geography",
}, },
{ {
"ariaLabel": "geometry-option",
"key": "Geometry", "key": "Geometry",
"text": "Geometry", "text": "Geometry",
}, },
@@ -1760,10 +1766,12 @@ exports[`SubSettingsComponent renders 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "geography-option",
"key": "Geography", "key": "Geography",
"text": "Geography", "text": "Geography",
}, },
{ {
"ariaLabel": "geometry-option",
"key": "Geometry", "key": "Geometry",
"text": "Geometry", "text": "Geometry",
}, },
@@ -2330,10 +2338,12 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
options={ options={
[ [
{ {
"ariaLabel": "geography-option",
"key": "Geography", "key": "Geography",
"text": "Geography", "text": "Geography",
}, },
{ {
"ariaLabel": "geometry-option",
"key": "Geometry", "key": "Geometry",
"text": "Geometry", "text": "Geometry",
}, },

View File

@@ -153,16 +153,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKey", "partitionKey",
], ],
"rawDataModel": { "rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": { "uniqueKeyPolicy": {
"uniqueKeys": [ "uniqueKeys": [
{ {
@@ -274,16 +264,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKey", "partitionKey",
], ],
"rawDataModel": { "rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": { "uniqueKeyPolicy": {
"uniqueKeys": [ "uniqueKeys": [
{ {
@@ -496,16 +476,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKey", "partitionKey",
], ],
"rawDataModel": { "rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": { "uniqueKeyPolicy": {
"uniqueKeys": [ "uniqueKeys": [
{ {
@@ -683,16 +653,6 @@ exports[`SettingsComponent renders 1`] = `
"partitionKey", "partitionKey",
], ],
"rawDataModel": { "rawDataModel": {
"indexingPolicy": {
"automatic": true,
"compositeIndexes": [],
"excludedPaths": [],
"fullTextIndexes": [],
"includedPaths": [],
"indexingMode": "consistent",
"spatialIndexes": [],
"vectorIndexes": [],
},
"uniqueKeyPolicy": { "uniqueKeyPolicy": {
"uniqueKeys": [ "uniqueKeys": [
{ {

View File

@@ -53,6 +53,7 @@ type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexTyp
const labelStyles = { const labelStyles = {
root: { root: {
fontSize: 12, fontSize: 12,
color: "var(--colorNeutralForeground1)",
}, },
}; };
@@ -63,6 +64,8 @@ const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldSt
field: { field: {
fontSize: 12, fontSize: 12,
padding: "0 8px", padding: "0 8px",
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)",
}, },
}; };

View File

@@ -437,13 +437,14 @@ export default class Explorer {
public onRefreshResourcesClick = async (): Promise<void> => { public onRefreshResourcesClick = async (): Promise<void> => {
if (isFabricMirroredKey()) { if (isFabricMirroredKey()) {
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases()); scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
return; } else {
}
await (userContext.authType === AuthType.ResourceToken await (userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases()); : this.refreshAllDatabases());
await this.refreshNotebookList(); await this.refreshNotebookList();
}
logConsoleInfo("Successfully refreshed databases");
}; };
// Facade // Facade

View File

@@ -853,7 +853,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isSynapseLinkEnabled() && ( {!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Text variant="small"> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
Azure Synapse Link is required for creating an analytical store{" "} Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br /> {getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
<Link <Link

View File

@@ -475,6 +475,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
className="panelGroupSpacing" className="panelGroupSpacing"
> >
<Text <Text
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
variant="small" variant="small"
> >
Azure Synapse Link is required for creating an analytical store Azure Synapse Link is required for creating an analytical store

View File

@@ -208,7 +208,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</div> </div>
</Stack> </Stack>
{createNewContainer ? ( {createNewContainer ? (
<Stack> <Stack data-test="create-new-container-form">
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar> <MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Stack horizontal> <Stack horizontal>
@@ -230,6 +230,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</TooltipHost> </TooltipHost>
</Stack> </Stack>
<input <input
data-test="new-container-id-input"
name="collectionId" name="collectionId"
id="collectionId" id="collectionId"
type="text" type="text"
@@ -271,6 +272,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
<input <input
type="text" type="text"
data-test="new-container-partition-key-input"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
aria-required aria-required
required required
@@ -304,6 +306,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
type="text" type="text"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
key={`addCollection-partitionKeyValue_${index}`} key={`addCollection-partitionKeyValue_${index}`}
data-test={`new-container-sub-partition-key-input-${index}`}
aria-required aria-required
required required
size={40} size={40}
@@ -327,6 +330,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}} }}
/> />
<IconButton <IconButton
data-test={`remove-sub-partition-key-button-${index}`}
ariaLabel="Remove hierarchical partition key"
iconProps={{ iconName: "Delete" }} iconProps={{ iconName: "Delete" }}
style={{ height: 27 }} style={{ height: 27 }}
onClick={() => { onClick={() => {
@@ -339,6 +344,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
})} })}
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<DefaultButton <DefaultButton
data-test="add-sub-partition-key-button"
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }} styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition} disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])} onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
@@ -346,7 +352,11 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
Add hierarchical partition key Add hierarchical partition key
</DefaultButton> </DefaultButton>
{subPartitionKeys.length > 0 && ( {subPartitionKeys.length > 0 && (
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text
data-test="hierarchical-partitioning-info-text"
variant="small"
style={{ color: "var(--colorNeutralForeground1)" }}
>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to <Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET V3, 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.{" "} Java V4 SDK, or preview JavaScript V3 SDK.{" "}
@@ -359,7 +369,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</Stack> </Stack>
</Stack> </Stack>
) : ( ) : (
<Stack> <Stack data-test="use-existing-container-form">
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
@@ -390,6 +400,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}} }}
defaultSelectedKey={targetCollectionId} defaultSelectedKey={targetCollectionId}
responsiveMode={999} responsiveMode={999}
ariaLabel="Existing Containers"
/> />
</Stack> </Stack>
)} )}

View File

@@ -2,7 +2,7 @@ import React from "react";
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
export const PanelLoadingScreen: React.FunctionComponent = () => ( export const PanelLoadingScreen: React.FunctionComponent = () => (
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer"> <div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerLoaderforcopyJobs">
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} /> <img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
</div> </div>
); );

View File

@@ -205,7 +205,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets." tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
/> />
{uploadFileData?.length > 0 && ( {uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer"> <div className="fileUploadSummaryContainer" data-test="file-upload-status">
<b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b> <b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b>
<DetailsList <DetailsList
items={uploadFileData} items={uploadFileData}

View File

@@ -61,7 +61,8 @@ const useStyles = makeStyles({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
minHeight: "100vh", height: "100%",
overflowY: "auto",
backgroundColor: "var(--colorNeutralBackground1)", backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
}, },
@@ -73,20 +74,19 @@ const useStyles = makeStyles({
}, },
title: { title: {
fontSize: "48px", fontSize: "48px",
fontWeight: "500", fontWeight: "400",
margin: "16px auto", margin: "16px auto",
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
}, },
subtitle: { subtitle: {
fontSize: "18px", fontSize: "18px",
marginBottom: "40px",
color: "var(--colorNeutralForeground2)", color: "var(--colorNeutralForeground2)",
}, },
cardContainer: { cardContainer: {
display: "grid", display: "grid",
gridTemplateColumns: "repeat(2, 1fr)", gridTemplateColumns: "repeat(2, 1fr)",
gap: "16px", gap: "16px",
width: "66%", width: "60%",
margin: "0 auto", margin: "0 auto",
backgroundColor: "var(--colorNeutralBackground1)", backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
@@ -100,7 +100,7 @@ const useStyles = makeStyles({
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
border: "1px solid var(--colorNeutralStroke1)", border: "1px solid var(--colorNeutralStroke1)",
borderRadius: "4px", borderRadius: "4px",
boxShadow: "var(--shadow4)", boxShadow: "rgba(0, 0, 0, 0.25) 0px 4px 4px",
cursor: "pointer", cursor: "pointer",
minHeight: "150px", minHeight: "150px",
"&:hover": { "&:hover": {
@@ -128,11 +128,10 @@ const useStyles = makeStyles({
textAlign: "left", textAlign: "left",
}, },
moreStuffContainer: { moreStuffContainer: {
display: "grid", display: "flex",
gridTemplateColumns: "repeat(3, 1fr)", justifyContent: "space-between",
gap: "32px", gap: "32px",
width: "66%", width: "90%",
margin: "40px auto",
}, },
moreStuffColumn: { moreStuffColumn: {
display: "flex", display: "flex",
@@ -227,7 +226,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
return ( return (
<Stack <Stack
className="splashStackContainer" className="splashStackContainer"
style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} style={{ width: "60%", cursor: "pointer", margin: "40px auto" }}
tokens={{ childrenGap: 16 }} tokens={{ childrenGap: 16 }}
> >
<Stack className="splashStackRow" horizontal> <Stack className="splashStackRow" horizontal>
@@ -903,9 +902,9 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
return ( return (
<div className={styles.splashScreenContainer}> <div className={styles.splashScreenContainer}>
<div className={styles.splashScreen}> <div className={styles.splashScreen}>
<h1 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB"> <h2 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
Welcome to Azure Cosmos DB<span className="activePatch"></span> Welcome to Azure Cosmos DB<span className="activePatch"></span>
</h1> </h2>
<div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div> <div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div>
{getSplashScreenButtons()} {getSplashScreenButtons()}
{useCarousel.getState().showCoachMark && ( {useCarousel.getState().showCoachMark && (

View File

@@ -15,7 +15,7 @@ const useStyles = makeStyles({
button: { button: {
border: "1px solid var(--colorNeutralStroke1)", border: "1px solid var(--colorNeutralStroke1)",
boxSizing: "border-box", boxSizing: "border-box",
boxShadow: "var(--shadow4)", boxShadow: "rgba(0, 0, 0, 0.25) 0px 4px 4px",
borderRadius: "4px", borderRadius: "4px",
padding: "32px 16px", padding: "32px 16px",
backgroundColor: "var(--colorNeutralBackground1)", backgroundColor: "var(--colorNeutralBackground1)",

View File

@@ -1,107 +0,0 @@
import { CircleFilled } from "@fluentui/react-icons";
import type { IIndexMetric } from "Explorer/Tabs/QueryTab/ResultsView";
import { useIndexAdvisorStyles } from "Explorer/Tabs/QueryTab/StylesAdvisor";
import * as React from "react";
// SDK response format
export interface IndexMetricsResponse {
UtilizedIndexes?: {
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
};
PotentialIndexes?: {
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
};
}
export function parseIndexMetrics(indexMetrics: IndexMetricsResponse): {
included: IIndexMetric[];
notIncluded: IIndexMetric[];
} {
const included: IIndexMetric[] = [];
const notIncluded: IIndexMetric[] = [];
// Process UtilizedIndexes (Included in Current Policy)
if (indexMetrics.UtilizedIndexes) {
// Single indexes
indexMetrics.UtilizedIndexes.SingleIndexes?.forEach((index) => {
included.push({
index: index.IndexSpec,
impact: index.IndexImpactScore || "Utilized",
section: "Included",
path: index.IndexSpec,
});
});
// Composite indexes
indexMetrics.UtilizedIndexes.CompositeIndexes?.forEach((index) => {
const compositeSpec = index.IndexSpecs.join(", ");
included.push({
index: compositeSpec,
impact: index.IndexImpactScore || "Utilized",
section: "Included",
composite: index.IndexSpecs.map((spec) => {
const [path, order] = spec.trim().split(/\s+/);
return {
path: path.trim(),
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
};
}),
});
});
}
// Process PotentialIndexes (Not Included in Current Policy)
if (indexMetrics.PotentialIndexes) {
// Single indexes
indexMetrics.PotentialIndexes.SingleIndexes?.forEach((index) => {
notIncluded.push({
index: index.IndexSpec,
impact: index.IndexImpactScore || "Unknown",
section: "Not Included",
path: index.IndexSpec,
});
});
// Composite indexes
indexMetrics.PotentialIndexes.CompositeIndexes?.forEach((index) => {
const compositeSpec = index.IndexSpecs.join(", ");
notIncluded.push({
index: compositeSpec,
impact: index.IndexImpactScore || "Unknown",
section: "Not Included",
composite: index.IndexSpecs.map((spec) => {
const [path, order] = spec.trim().split(/\s+/);
return {
path: path.trim(),
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
};
}),
});
});
}
return { included, notIncluded };
}
export const renderImpactDots = (impact: string): JSX.Element => {
const style = useIndexAdvisorStyles();
let count = 0;
if (impact === "High") {
count = 3;
} else if (impact === "Medium") {
count = 2;
} else if (impact === "Low") {
count = 1;
}
return (
<div className={style.indexAdvisorImpactDots}>
{Array.from({ length: count }).map((_, i) => (
<CircleFilled key={i} className={style.indexAdvisorImpactDot} />
))}
</div>
);
};

View File

@@ -3,21 +3,18 @@ import QueryError from "Common/QueryError";
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar"; import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
import { MessageBanner } from "Explorer/Controls/MessageBanner"; import { MessageBanner } from "Explorer/Controls/MessageBanner";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles"; import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import useZoomLevel from "hooks/useZoomLevel";
import React from "react"; import React from "react";
import { conditionalClass } from "Utils/StyleUtils";
import RunQuery from "../../../../images/RunQuery.png"; import RunQuery from "../../../../images/RunQuery.png";
import { QueryResults } from "../../../Contracts/ViewModels"; import { QueryResults } from "../../../Contracts/ViewModels";
import { ErrorList } from "./ErrorList"; import { ErrorList } from "./ErrorList";
import { ResultsView } from "./ResultsView"; import { ResultsView } from "./ResultsView";
import useZoomLevel from "hooks/useZoomLevel";
import { conditionalClass } from "Utils/StyleUtils";
export interface ResultsViewProps { export interface ResultsViewProps {
isMongoDB: boolean; isMongoDB: boolean;
queryResults: QueryResults; queryResults: QueryResults;
executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>; executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>;
queryEditorContent?: string;
databaseId?: string;
containerId?: string;
} }
interface QueryResultProps extends ResultsViewProps { interface QueryResultProps extends ResultsViewProps {
@@ -52,8 +49,6 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
queryResults, queryResults,
executeQueryDocumentsPage, executeQueryDocumentsPage,
isExecuting, isExecuting,
databaseId,
containerId,
}: QueryResultProps): JSX.Element => { }: QueryResultProps): JSX.Element => {
const styles = useQueryTabStyles(); const styles = useQueryTabStyles();
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent); const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
@@ -96,9 +91,6 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
queryResults={queryResults} queryResults={queryResults}
executeQueryDocumentsPage={executeQueryDocumentsPage} executeQueryDocumentsPage={executeQueryDocumentsPage}
isMongoDB={isMongoDB} isMongoDB={isMongoDB}
queryEditorContent={queryEditorContent}
databaseId={databaseId}
containerId={containerId}
/> />
) : ( ) : (
<ExecuteQueryCallToAction /> <ExecuteQueryCallToAction />

View File

@@ -52,9 +52,8 @@ describe("QueryTabComponent", () => {
copilotVersion: "v3.0", copilotVersion: "v3.0",
}, },
}); });
const propsMock: Readonly<IQueryTabComponentProps> = { const propsMock: Readonly<IQueryTabComponentProps> = {
collection: { databaseId: "CopilotSampleDB", id: () => "CopilotContainer" }, collection: { databaseId: "CopilotSampleDB" },
onTabAccessor: () => jest.fn(), onTabAccessor: () => jest.fn(),
isExecutionError: false, isExecutionError: false,
tabId: "mockTabId", tabId: "mockTabId",

View File

@@ -28,7 +28,6 @@ import { useMonacoTheme } from "hooks/useTheme";
import React, { Fragment, createRef } from "react"; import React, { Fragment, createRef } from "react";
import "react-splitter-layout/lib/index.css"; import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format"; import { format } from "react-string-format";
import create from "zustand";
//TODO: Uncomment next two lines when query copilot is reinstated in DE //TODO: Uncomment next two lines when query copilot is reinstated in DE
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg"; // import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; // import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
@@ -58,20 +57,6 @@ import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
import "./QueryTabComponent.less"; import "./QueryTabComponent.less";
export interface QueryMetadataStore {
userQuery: string;
databaseId: string;
containerId: string;
setMetadata: (query1: string, db: string, container: string) => void;
}
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
userQuery: "",
databaseId: "",
containerId: "",
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
}));
enum ToggleState { enum ToggleState {
Result, Result,
QueryMetrics, QueryMetrics,
@@ -279,10 +264,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
} }
public onExecuteQueryClick = async (): Promise<void> => { public onExecuteQueryClick = async (): Promise<void> => {
const query1 = this.state.sqlQueryEditorContent;
const db = this.props.collection.databaseId;
const container = this.props.collection.id();
useQueryMetadataStore.getState().setMetadata(query1, db, container);
this._iterator = undefined; this._iterator = undefined;
setTimeout(async () => { setTimeout(async () => {
@@ -799,8 +780,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
errors={this.props.copilotStore?.errors} errors={this.props.copilotStore?.errors}
isExecuting={this.props.copilotStore?.isExecuting} isExecuting={this.props.copilotStore?.isExecuting}
queryResults={this.props.copilotStore?.queryResults} queryResults={this.props.copilotStore?.queryResults}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
executeQueryDocumentsPage={(firstItemIndex: number) => executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage( QueryDocumentsPerPage(
firstItemIndex, firstItemIndex,
@@ -816,8 +795,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
errors={this.state.errors} errors={this.state.errors}
isExecuting={this.state.isExecuting} isExecuting={this.state.isExecuting}
queryResults={this.state.queryResults} queryResults={this.state.queryResults}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
executeQueryDocumentsPage={(firstItemIndex: number) => executeQueryDocumentsPage={(firstItemIndex: number) =>
this._executeQueryDocumentsPage(firstItemIndex) this._executeQueryDocumentsPage(firstItemIndex)
} }

View File

@@ -1,170 +0,0 @@
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import { IndexAdvisorTab } from "Explorer/Tabs/QueryTab/ResultsView";
import React from "react";
const mockReplace = jest.fn();
const mockFetchAll = jest.fn();
const mockRead = jest.fn();
const mockLogConsoleProgress = jest.fn();
const mockHandleError = jest.fn();
const indexMetricsResponse = {
UtilizedIndexes: {
SingleIndexes: [{ IndexSpec: "/foo/?", IndexImpactScore: "High" }],
CompositeIndexes: [{ IndexSpecs: ["/baz/? DESC", "/qux/? ASC"], IndexImpactScore: "Low" }],
},
PotentialIndexes: {
SingleIndexes: [{ IndexSpec: "/bar/?", IndexImpactScore: "Medium" }],
CompositeIndexes: [] as Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>,
},
};
const mockQueryResults = {
documents: [] as unknown[],
hasMoreResults: false,
itemCount: 0,
firstItemIndex: 0,
lastItemIndex: 0,
requestCharge: 0,
activityId: "test-activity-id",
};
mockRead.mockResolvedValue({
resource: {
indexingPolicy: {
automatic: true,
indexingMode: "consistent",
includedPaths: [{ path: "/*" }, { path: "/foo/?" }],
excludedPaths: [],
},
partitionKey: "pk",
},
});
mockReplace.mockResolvedValue({
resource: {
indexingPolicy: {
automatic: true,
indexingMode: "consistent",
includedPaths: [{ path: "/*" }],
excludedPaths: [],
},
},
});
jest.mock("Common/CosmosClient", () => ({
client: () => ({
database: () => ({
container: () => ({
items: {
query: () => ({
fetchAll: mockFetchAll,
}),
},
read: mockRead,
replace: mockReplace,
}),
}),
}),
}));
jest.mock("./StylesAdvisor", () => ({
useIndexAdvisorStyles: () => ({}),
}));
jest.mock("../../../Utils/NotificationConsoleUtils", () => ({
logConsoleProgress: (...args: unknown[]) => {
mockLogConsoleProgress(...args);
return () => {};
},
}));
jest.mock("../../../Common/ErrorHandlingUtils", () => ({
handleError: (...args: unknown[]) => mockHandleError(...args),
}));
beforeEach(() => {
jest.clearAllMocks();
mockFetchAll.mockResolvedValue({ indexMetrics: indexMetricsResponse });
});
describe("IndexAdvisorTab Basic Tests", () => {
test("component renders without crashing", () => {
const { container } = render(
<IndexAdvisorTab queryEditorContent="SELECT * FROM c" databaseId="db1" containerId="col1" />,
);
expect(container).toBeTruthy();
});
test("renders component and handles missing parameters", () => {
const { container } = render(<IndexAdvisorTab />);
expect(container).toBeTruthy();
// Should not crash when parameters are missing
});
test("fetches index metrics with query results", async () => {
render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="db1"
containerId="col1"
/>,
);
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
});
test("displays content after loading", async () => {
render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="db1"
containerId="col1"
/>,
);
// Wait for the component to finish loading
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
// Component should have rendered some content
expect(screen.getByText(/Index Advisor/i)).toBeInTheDocument();
});
test("calls log console progress when fetching metrics", async () => {
render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="db1"
containerId="col1"
/>,
);
await waitFor(() => expect(mockLogConsoleProgress).toHaveBeenCalled());
});
test("handles error when fetch fails", async () => {
mockFetchAll.mockRejectedValueOnce(new Error("fetch failed"));
render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="db1"
containerId="col1"
/>,
);
await waitFor(() => expect(mockHandleError).toHaveBeenCalled(), { timeout: 3000 });
});
test("renders with all required props", () => {
const { container } = render(
<IndexAdvisorTab
queryResults={mockQueryResults}
queryEditorContent="SELECT * FROM c"
databaseId="testDb"
containerId="testContainer"
/>,
);
expect(container).toBeTruthy();
expect(container.firstChild).toBeTruthy();
});
});

View File

@@ -1,8 +1,5 @@
import type { CompositePath, IndexingPolicy } from "@azure/cosmos";
import { FontIcon } from "@fluentui/react";
import { import {
Button, Button,
Checkbox,
DataGrid, DataGrid,
DataGridBody, DataGridBody,
DataGridCell, DataGridCell,
@@ -11,45 +8,28 @@ import {
DataGridRow, DataGridRow,
SelectTabData, SelectTabData,
SelectTabEvent, SelectTabEvent,
Spinner,
Tab, Tab,
TabList, TabList,
Table,
TableBody,
TableCell,
TableColumnDefinition, TableColumnDefinition,
TableHeader,
TableRow,
createTableColumn, createTableColumn,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { ArrowDownloadRegular, ChevronDown20Regular, ChevronRight20Regular, CopyRegular } from "@fluentui/react-icons"; import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons";
import copy from "clipboard-copy";
import { HttpHeaders } from "Common/Constants"; import { HttpHeaders } from "Common/Constants";
import MongoUtility from "Common/MongoUtility"; import MongoUtility from "Common/MongoUtility";
import { QueryMetrics } from "Contracts/DataModels"; import { QueryMetrics } from "Contracts/DataModels";
import { QueryResults } from "Contracts/ViewModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import {
parseIndexMetrics,
renderImpactDots,
type IndexMetricsResponse,
} from "Explorer/Tabs/QueryTab/IndexAdvisorUtils";
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent"; import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles"; import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import React, { useCallback, useEffect, useState } from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils"; import copy from "clipboard-copy";
import create from "zustand"; import React, { useCallback, useState } from "react";
import { client } from "../../../Common/CosmosClient";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { sampleDataClient } from "../../../Common/SampleDataClient";
import { ResultsViewProps } from "./QueryResultSection"; import { ResultsViewProps } from "./QueryResultSection";
import { useIndexAdvisorStyles } from "./StylesAdvisor";
enum ResultsTabs { enum ResultsTabs {
Results = "results", Results = "results",
QueryStats = "queryStats", QueryStats = "queryStats",
IndexAdvisor = "indexadv",
} }
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => { const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
const styles = useQueryTabStyles(); const styles = useQueryTabStyles();
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
@@ -543,331 +523,14 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
); );
}; };
export interface IIndexMetric { export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
index: string;
impact: string;
section: "Included" | "Not Included" | "Header";
path?: string;
composite?: { path: string; order: string }[];
}
export const IndexAdvisorTab: React.FC<{
queryResults?: QueryResults;
queryEditorContent?: string;
databaseId?: string;
containerId?: string;
}> = ({ queryResults, queryEditorContent, databaseId, containerId }) => {
const style = useIndexAdvisorStyles();
const [loading, setLoading] = useState(false);
const [indexMetrics, setIndexMetrics] = useState<IndexMetricsResponse | null>(null);
const [showIncluded, setShowIncluded] = useState(true);
const [showNotIncluded, setShowNotIncluded] = useState(true);
const [selectedIndexes, setSelectedIndexes] = useState<IIndexMetric[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [updateMessageShown, setUpdateMessageShown] = useState(false);
const [included, setIncludedIndexes] = useState<IIndexMetric[]>([]);
const [notIncluded, setNotIncludedIndexes] = useState<IIndexMetric[]>([]);
const [isUpdating, setIsUpdating] = useState(false);
const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
const indexingMetricsDocLink = "https://learn.microsoft.com/azure/cosmos-db/nosql/index-metrics";
const fetchIndexMetrics = async () => {
if (!queryEditorContent || !databaseId || !containerId) {
return;
}
setLoading(true);
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
try {
const querySpec = {
query: queryEditorContent,
};
// Use sampleDataClient for CopilotSampleDB, regular client for other databases
const cosmosClient = databaseId === "CopilotSampleDB" ? sampleDataClient() : client();
const sdkResponse = await cosmosClient
.database(databaseId)
.container(containerId)
.items.query(querySpec, {
populateIndexMetrics: true,
})
.fetchAll();
const parsedMetrics =
typeof sdkResponse.indexMetrics === "string" ? JSON.parse(sdkResponse.indexMetrics) : sdkResponse.indexMetrics;
setIndexMetrics(parsedMetrics);
} catch (error) {
handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`);
} finally {
clearMessage();
setLoading(false);
}
};
// Fetch index metrics when query results change (i.e., when Execute Query is clicked)
useEffect(() => {
if (queryEditorContent && databaseId && containerId && queryResults) {
fetchIndexMetrics();
}
}, [queryResults]);
useEffect(() => {
if (!indexMetrics) {
return;
}
const { included, notIncluded } = parseIndexMetrics(indexMetrics);
setIncludedIndexes(included);
setNotIncludedIndexes(notIncluded);
if (justUpdatedPolicy) {
setJustUpdatedPolicy(false);
} else {
setUpdateMessageShown(false);
}
}, [indexMetrics]);
useEffect(() => {
const allSelected =
notIncluded.length > 0 && notIncluded.every((item) => selectedIndexes.some((s) => s.index === item.index));
setSelectAll(allSelected);
}, [selectedIndexes, notIncluded]);
const handleCheckboxChange = (indexObj: IIndexMetric, checked: boolean) => {
if (checked) {
setSelectedIndexes((prev) => [...prev, indexObj]);
} else {
setSelectedIndexes((prev) => prev.filter((item) => item.index !== indexObj.index));
}
};
const handleSelectAll = (checked: boolean) => {
setSelectAll(checked);
setSelectedIndexes(checked ? notIncluded : []);
};
const handleUpdatePolicy = async () => {
setIsUpdating(true);
try {
const containerRef = client().database(databaseId).container(containerId);
const { resource: containerDef } = await containerRef.read();
const newIncludedPaths = selectedIndexes
.filter((index) => !index.composite)
.map((index) => {
return {
path: index.path,
};
});
const newCompositeIndexes: CompositePath[][] = selectedIndexes
.filter((index) => Array.isArray(index.composite))
.map(
(index) =>
(index.composite as { path: string; order: string }[]).map((comp) => ({
path: comp.path,
order: comp.order === "descending" ? "descending" : "ascending",
})) as CompositePath[],
);
const updatedPolicy: IndexingPolicy = {
...containerDef.indexingPolicy,
includedPaths: [...(containerDef.indexingPolicy?.includedPaths || []), ...newIncludedPaths],
compositeIndexes: [...(containerDef.indexingPolicy?.compositeIndexes || []), ...newCompositeIndexes],
automatic: containerDef.indexingPolicy?.automatic ?? true,
indexingMode: containerDef.indexingPolicy?.indexingMode ?? "consistent",
excludedPaths: containerDef.indexingPolicy?.excludedPaths ?? [],
};
await containerRef.replace({
id: containerId,
partitionKey: containerDef.partitionKey,
indexingPolicy: updatedPolicy,
});
useIndexingPolicyStore.getState().setIndexingPolicyFor(containerId, updatedPolicy);
const selectedIndexSet = new Set(selectedIndexes.map((s) => s.index));
const updatedNotIncluded: typeof notIncluded = [];
const newlyIncluded: typeof included = [];
for (const item of notIncluded) {
if (selectedIndexSet.has(item.index)) {
newlyIncluded.push(item);
} else {
updatedNotIncluded.push(item);
}
}
const newIncluded = [...included, ...newlyIncluded];
const newNotIncluded = updatedNotIncluded;
setIncludedIndexes(newIncluded);
setNotIncludedIndexes(newNotIncluded);
setSelectedIndexes([]);
setSelectAll(false);
setUpdateMessageShown(true);
setJustUpdatedPolicy(true);
} catch (err) {
console.error("Failed to update indexing policy:", err);
} finally {
setIsUpdating(false);
}
};
const renderRow = (item: IIndexMetric, index: number) => {
const isHeader = item.section === "Header";
const isNotIncluded = item.section === "Not Included";
return (
<TableRow key={index}>
<TableCell colSpan={2}>
<div className={style.indexAdvisorGrid}>
{isNotIncluded ? (
<Checkbox
checked={selectedIndexes.some((selected) => selected.index === item.index)}
onChange={(_, data) => handleCheckboxChange(item, data.checked === true)}
/>
) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? (
<Checkbox checked={selectAll} onChange={(_, data) => handleSelectAll(data.checked === true)} />
) : (
<div className={style.indexAdvisorCheckboxSpacer}></div>
)}
{isHeader ? (
<span
style={{ cursor: "pointer" }}
onClick={() => {
if (item.index === "Included in Current Policy") {
setShowIncluded(!showIncluded);
} else if (item.index === "Not Included in Current Policy") {
setShowNotIncluded(!showNotIncluded);
}
}}
>
{item.index === "Included in Current Policy" ? (
showIncluded ? (
<ChevronDown20Regular />
) : (
<ChevronRight20Regular />
)
) : showNotIncluded ? (
<ChevronDown20Regular />
) : (
<ChevronRight20Regular />
)}
</span>
) : (
<div className={style.indexAdvisorChevronSpacer}></div>
)}
<div className={isHeader ? style.indexAdvisorRowBold : style.indexAdvisorRowNormal}>{item.index}</div>
<div className={isHeader ? style.indexAdvisorRowImpactHeader : style.indexAdvisorRowImpact}>
{!isHeader && item.impact}
</div>
<div>{!isHeader && renderImpactDots(item.impact)}</div>
</div>
</TableCell>
</TableRow>
);
};
const indexMetricItems = React.useMemo(() => {
const items: IIndexMetric[] = [];
items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" });
if (showNotIncluded) {
notIncluded.forEach((item) => items.push({ ...item, section: "Not Included" }));
}
items.push({ index: "Included in Current Policy", impact: "", section: "Header" });
if (showIncluded) {
included.forEach((item) => items.push({ ...item, section: "Included" }));
}
return items;
}, [included, notIncluded, showIncluded, showNotIncluded]);
if (loading) {
return (
<div>
<Spinner
size="small"
style={
{
"--spinner-size": "16px",
"--spinner-thickness": "2px",
"--spinner-color": "#0078D4",
} as React.CSSProperties
}
/>
</div>
);
}
return (
<div>
<div className={style.indexAdvisorMessage}>
{updateMessageShown ? (
<>
<span className={style.indexAdvisorSuccessIcon}>
<FontIcon iconName="CheckMark" style={{ color: "white", fontSize: 12 }} />
</span>
<span>
Your indexing policy has been updated with the new included paths. You may review the changes in Scale &
Settings.
</span>
</>
) : (
<>
<span>
Index Advisor uses Indexing Metrics to suggest query paths that, when included in your indexing policy,
can improve the performance of this query by reducing RU costs and lowering latency.{" "}
<a href={indexingMetricsDocLink} target="_blank" rel="noopener noreferrer">
Learn more about Indexing Metrics
</a>
.{" "}
</span>
</>
)}
</div>
<div className={style.indexAdvisorTitle}>Indexes analysis</div>
<Table className={style.indexAdvisorTable}>
<TableHeader>
<TableRow>
<TableCell colSpan={2}>
<div className={style.indexAdvisorGrid}>
<div className={style.indexAdvisorCheckboxSpacer}></div>
<div className={style.indexAdvisorChevronSpacer}></div>
<div>Index</div>
<div>
<span style={{ whiteSpace: "nowrap" }}>Estimated Impact</span>
</div>
</div>
</TableCell>
</TableRow>
</TableHeader>
<TableBody>{indexMetricItems.map(renderRow)}</TableBody>
</Table>
{selectedIndexes.length > 0 && (
<div className={style.indexAdvisorButtonBar}>
{isUpdating ? (
<div className={style.indexAdvisorButtonSpinner}>
<Spinner size="tiny" />{" "}
</div>
) : (
<button onClick={handleUpdatePolicy} className={style.indexAdvisorButton}>
Update Indexing Policy with selected index(es)
</button>
)}
</div>
)}
</div>
);
};
export const ResultsView: React.FC<ResultsViewProps> = ({
isMongoDB,
queryResults,
executeQueryDocumentsPage,
queryEditorContent,
databaseId,
containerId,
}) => {
const styles = useQueryTabStyles(); const styles = useQueryTabStyles();
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results); const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => { const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
setActiveTab(data.value as ResultsTabs); setActiveTab(data.value as ResultsTabs);
}, []); }, []);
return ( return (
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}> <div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}> <TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
@@ -885,13 +548,6 @@ export const ResultsView: React.FC<ResultsViewProps> = ({
> >
Query Stats Query Stats
</Tab> </Tab>
<Tab
data-test="QueryTab/ResultsPane/ResultsView/IndexAdvisorTab"
id={ResultsTabs.IndexAdvisor}
value={ResultsTabs.IndexAdvisor}
>
Index Advisor
</Tab>
</TabList> </TabList>
<div className={styles.queryResultsTabContentContainer}> <div className={styles.queryResultsTabContentContainer}>
{activeTab === ResultsTabs.Results && ( {activeTab === ResultsTabs.Results && (
@@ -902,30 +558,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({
/> />
)} )}
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />} {activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
{activeTab === ResultsTabs.IndexAdvisor && (
<IndexAdvisorTab
queryResults={queryResults}
queryEditorContent={queryEditorContent}
databaseId={databaseId}
containerId={containerId}
/>
)}
</div> </div>
</div> </div>
); );
}; };
export interface IndexingPolicyStore {
indexingPolicies: { [containerId: string]: IndexingPolicy };
setIndexingPolicyFor: (containerId: string, indexingPolicy: IndexingPolicy) => void;
}
export const useIndexingPolicyStore = create<IndexingPolicyStore>((set) => ({
indexingPolicies: {},
setIndexingPolicyFor: (containerId, indexingPolicy) =>
set((state) => ({
indexingPolicies: {
...state.indexingPolicies,
[containerId]: { ...indexingPolicy },
},
})),
}));

View File

@@ -1,95 +0,0 @@
import { makeStyles } from "@fluentui/react-components";
export type IndexAdvisorStyles = ReturnType<typeof useIndexAdvisorStyles>;
export const useIndexAdvisorStyles = makeStyles({
indexAdvisorMessage: {
padding: "1rem",
fontSize: "1.2rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
},
indexAdvisorSuccessIcon: {
width: "18px",
height: "18px",
borderRadius: "50%",
backgroundColor: "#107C10",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
indexAdvisorTitle: {
padding: "1rem",
fontSize: "1.3rem",
fontWeight: "bold",
},
indexAdvisorTable: {
display: "block",
alignItems: "center",
marginBottom: "7rem",
},
indexAdvisorGrid: {
display: "grid",
gridTemplateColumns: "30px 30px 1fr 50px 120px",
alignItems: "center",
gap: "15px",
fontWeight: "bold",
},
indexAdvisorCheckboxSpacer: {
width: "18px",
height: "18px",
},
indexAdvisorChevronSpacer: {
width: "24px",
},
indexAdvisorRowBold: {
fontWeight: "bold",
},
indexAdvisorRowNormal: {
fontWeight: "normal",
},
indexAdvisorRowImpactHeader: {
fontSize: 0,
},
indexAdvisorRowImpact: {
fontWeight: "normal",
},
indexAdvisorImpactDot: {
color: "#0078D4",
fontSize: "12px",
display: "inline-flex",
},
indexAdvisorImpactDots: {
display: "flex",
alignItems: "center",
gap: "4px",
},
indexAdvisorButtonBar: {
padding: "1rem",
marginTop: "-7rem",
flexWrap: "wrap",
},
indexAdvisorButtonSpinner: {
marginTop: "1rem",
minWidth: "320px",
minHeight: "40px",
display: "flex",
alignItems: "left",
justifyContent: "left",
marginLeft: "10rem",
},
indexAdvisorButton: {
backgroundColor: "#0078D4",
color: "white",
padding: "8px 16px",
border: "none",
borderRadius: "4px",
cursor: "pointer",
marginTop: "1rem",
fontSize: "1rem",
fontWeight: 500,
transition: "background 0.2s",
":hover": {
backgroundColor: "#005a9e",
},
},
});

View File

@@ -1,15 +0,0 @@
import create from "zustand";
interface QueryMetadataStore {
userQuery: string;
databaseId: string;
containerId: string;
setMetadata: (query1: string, db: string, container: string) => void;
}
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
userQuery: "",
databaseId: "",
containerId: "",
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
}));

View File

@@ -435,7 +435,6 @@ export default class StoredProcedureTabComponent extends React.Component<
}); });
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
}, 100); }, 100);
return createdResource; return createdResource;
}, },
(createError) => { (createError) => {

View File

@@ -125,7 +125,10 @@ const App = (): JSX.Element => {
<KeyboardShortcutRoot> <KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false"> <div className="flexContainer" aria-hidden="false">
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? ( {userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
<>
<ContainerCopyPanel explorer={explorer} /> <ContainerCopyPanel explorer={explorer} />
<SidePanel />
</>
) : ( ) : (
<DivExplorer explorer={explorer} /> <DivExplorer explorer={explorer} />
)} )}

View File

@@ -316,11 +316,6 @@ body.isDarkMode {
background-color: transparent; background-color: transparent;
} }
// High specificity override for any nested elements
* {
color: var(--colorNeutralForeground1);
}
// Ensure links maintain proper colors // Ensure links maintain proper colors
.ms-Link { .ms-Link {
color: var(--colorBrandForeground1); color: var(--colorBrandForeground1);
@@ -438,7 +433,6 @@ body.isDarkMode {
button { button {
&:not(.ms-Button):not(.ms-IconButton) { &:not(.ms-Button):not(.ms-IconButton) {
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1); color: var(--colorNeutralForeground1);
&:hover { &:hover {

View File

@@ -12,6 +12,7 @@
--colorCompoundBrandStroke1: @SelectionColor; --colorCompoundBrandStroke1: @SelectionColor;
--colorBrandForeground1: @LinkColor; --colorBrandForeground1: @LinkColor;
--colorPaletteRedForeground1: @ErrorColor; --colorPaletteRedForeground1: @ErrorColor;
--colorSuccessGreen: #107c10;
--overlayBackground: rgba(0, 0, 0, 0.4); --overlayBackground: rgba(0, 0, 0, 0.4);
--colorBrandBackground: @SelectionColor; --colorBrandBackground: @SelectionColor;
--colorBrandBackgroundHover: @AccentMediumHigh; --colorBrandBackgroundHover: @AccentMediumHigh;
@@ -32,6 +33,7 @@ body.isDarkMode {
--colorCompoundBrandStroke1: #4db6e8; --colorCompoundBrandStroke1: #4db6e8;
--colorBrandForeground1: #4db6e8; --colorBrandForeground1: #4db6e8;
--colorPaletteRedForeground1: #f25d5d; --colorPaletteRedForeground1: #f25d5d;
--colorSuccessGreen: #107c10;
--overlayBackground: rgba(0, 0, 0, 0.4); --overlayBackground: rgba(0, 0, 0, 0.4);
--colorBrandBackground: #0078d4; --colorBrandBackground: #0078d4;
--colorBrandBackgroundHover: #106ebe; --colorBrandBackgroundHover: #106ebe;

View File

@@ -164,6 +164,9 @@ $ENV:NOSQL_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<accou
# NoSQL API (Readonly) # NoSQL API (Readonly)
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken $ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
# NoSQL API (Container Copy)
$ENV:NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
# Tables API # Tables API
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken $ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken

View File

@@ -8,7 +8,8 @@ test("Cassandra keyspace and table CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.Cassandra); const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
await explorer.globalCommandButton("New Table").click(); const newTableButton = await explorer.globalCommandButton("New Table");
await newTableButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"Add Table", "Add Table",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -11,7 +11,7 @@ export interface TestNameOptions {
prefixed?: boolean; prefixed?: boolean;
} }
export function generateUniqueName(baseName, options?: TestNameOptions): string { export function generateUniqueName(baseName: string, options?: TestNameOptions): string {
const length = options?.length ?? 1; const length = options?.length ?? 1;
const timestamp = options?.timestampped === undefined ? true : options.timestampped; const timestamp = options?.timestampped === undefined ? true : options.timestampped;
const prefixed = options?.prefixed === undefined ? true : options.prefixed; const prefixed = options?.prefixed === undefined ? true : options.prefixed;
@@ -40,6 +40,7 @@ export enum TestAccount {
Mongo32 = "Mongo32", Mongo32 = "Mongo32",
SQL = "SQL", SQL = "SQL",
SQLReadOnly = "SQLReadOnly", SQLReadOnly = "SQLReadOnly",
SQLContainerCopyOnly = "SQLContainerCopyOnly",
} }
export const defaultAccounts: Record<TestAccount, string> = { export const defaultAccounts: Record<TestAccount, string> = {
@@ -51,6 +52,7 @@ export const defaultAccounts: Record<TestAccount, string> = {
[TestAccount.Mongo32]: "github-e2etests-mongo32", [TestAccount.Mongo32]: "github-e2etests-mongo32",
[TestAccount.SQL]: "github-e2etests-sql", [TestAccount.SQL]: "github-e2etests-sql",
[TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly",
[TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly",
}; };
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests"; export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
@@ -77,7 +79,14 @@ export function getAccountName(accountType: TestAccount) {
); );
} }
export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: string): Promise<string> { type TestExplorerUrlOptions = {
iframeSrc?: string;
enablecontainercopy?: boolean;
};
export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise<string> {
const { iframeSrc, enablecontainercopy } = options ?? {};
// We can't retrieve AZ CLI credentials from the browser so we get them here. // We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken(); const token = await getAzureCLICredentialsToken();
const accountName = getAccountName(accountType); const accountName = getAccountName(accountType);
@@ -93,6 +102,7 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN; const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN;
const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN; const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN;
const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN; const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN;
const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN; const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN;
@@ -108,6 +118,16 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
} }
break; break;
case TestAccount.SQLContainerCopyOnly:
if (nosqlContainerCopyRbacToken) {
params.set("nosqlRbacToken", nosqlContainerCopyRbacToken);
params.set("enableaaddataplane", "true");
}
if (enablecontainercopy) {
params.set("enablecontainercopy", "true");
}
break;
case TestAccount.SQLReadOnly: case TestAccount.SQLReadOnly:
if (nosqlReadOnlyRbacToken) { if (nosqlReadOnlyRbacToken) {
params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken); params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken);
@@ -165,6 +185,39 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
return `https://localhost:1234/testExplorer.html?${params.toString()}`; return `https://localhost:1234/testExplorer.html?${params.toString()}`;
} }
type DropdownItemExpectations = {
ariaLabel?: string;
itemCount?: number;
};
type DropdownItemMatcher = {
name?: string;
position?: number;
};
export async function getDropdownItemByNameOrPosition(
frame: Frame,
matcher?: DropdownItemMatcher,
expectedOptions?: DropdownItemExpectations,
): Promise<Locator> {
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
if (expectedOptions?.ariaLabel) {
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel);
}
if (expectedOptions?.itemCount) {
const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
await expect(items).toHaveCount(expectedOptions.itemCount);
}
const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
if (matcher?.name) {
return containerDropdownItems.filter({ hasText: matcher.name });
} else if (matcher?.position !== undefined) {
return containerDropdownItems.nth(matcher.position);
}
// Return first item if no matcher is provided
return containerDropdownItems.first();
}
/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */ /** Helper class that provides locator methods for TreeNode elements, on top of a Locator */
class TreeNode { class TreeNode {
constructor( constructor(
@@ -325,7 +378,9 @@ type PanelOpenOptions = {
export enum CommandBarButton { export enum CommandBarButton {
Save = "Save", Save = "Save",
Execute = "Execute",
ExecuteQuery = "Execute Query", ExecuteQuery = "Execute Query",
UploadItem = "Upload Item",
} }
/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */ /** Helper class that provides locator methods for DataExplorer components, on top of a Frame */
@@ -352,8 +407,9 @@ export class DataExplorer {
* *
* There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button.
*/ */
globalCommandButton(label: string): Locator { async globalCommandButton(label: string): Promise<Locator> {
return this.frame.getByTestId("GlobalCommands").getByText(label); await this.frame.getByTestId("GlobalCommands").click();
return this.frame.getByRole("menuitem", { name: label });
} }
/** Select the command bar button with the specified label */ /** Select the command bar button with the specified label */
@@ -459,6 +515,15 @@ export class DataExplorer {
const containerNode = await this.waitForContainerNode(context.database.id, context.container.id); const containerNode = await this.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand(); await containerNode.expand();
// refresh tree to remove deleted database
const consoleMessages = await this.getNotificationConsoleMessages();
const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton");
await refreshButton.click();
await expect(consoleMessages).toContainText("Successfully refreshed databases", {
timeout: ONE_MINUTE_MS,
});
await this.collapseNotificationConsole();
const scaleAndSettingsButton = this.frame.getByTestId( const scaleAndSettingsButton = this.frame.getByTestId(
`TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`, `TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`,
); );
@@ -466,12 +531,46 @@ export class DataExplorer {
} }
/** Gets the console message element */ /** Gets the console message element */
getConsoleMessage(): Locator { getConsoleHeaderStatus(): Locator {
return this.frame.getByTestId("notification-console/header-status"); return this.frame.getByTestId("notification-console/header-status");
} }
async expandNotificationConsole(): Promise<void> {
await this.setNotificationConsoleExpanded(true);
}
async collapseNotificationConsole(): Promise<void> {
await this.setNotificationConsoleExpanded(false);
}
async setNotificationConsoleExpanded(expanded: boolean): Promise<void> {
const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton");
const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt");
// When expanded, the icon says "Collapse icon"
if (expanded && alt === "Expand icon") {
await notificationConsoleToggleButton.click();
} else if (!expanded && alt === "Collapse icon") {
await notificationConsoleToggleButton.click();
}
}
async getNotificationConsoleMessages(): Promise<Locator> {
await this.setNotificationConsoleExpanded(true);
return this.frame.getByTestId("NotificationConsole/Contents");
}
async getDropdownItemByName(name: string, ariaLabel?: string): Promise<Locator> {
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 */ /** Waits for the Data Explorer app to load */
static async waitForExplorer(page: Page) { static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise<DataExplorer> {
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();
if (iframeElement === null) { if (iframeElement === null) {
throw new Error("Explorer iframe not found"); throw new Error("Explorer iframe not found");
@@ -483,15 +582,126 @@ export class DataExplorer {
throw new Error("Explorer frame not found"); throw new Error("Explorer frame not found");
} }
if (!options?.enablecontainercopy) {
await explorerFrame?.getByTestId("DataExplorerRoot").waitFor(); await explorerFrame?.getByTestId("DataExplorerRoot").waitFor();
}
return new DataExplorer(explorerFrame); return new DataExplorer(explorerFrame);
} }
/** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */ /** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<DataExplorer> { static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<DataExplorer> {
const url = await getTestExplorerUrl(testAccount, iframeSrc); const url = await getTestExplorerUrl(testAccount, { iframeSrc });
await page.goto(url); await page.goto(url);
return DataExplorer.waitForExplorer(page); return DataExplorer.waitForExplorer(page);
} }
} }
export async function waitForApiResponse(
page: Page,
urlPattern: string,
method?: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payloadValidator?: (payload: any) => boolean,
) {
try {
// Check if page is still valid before waiting
if (page.isClosed()) {
throw new Error(`Page is closed, cannot wait for API response: ${urlPattern}`);
}
return page.waitForResponse(
async (response) => {
const request = response.request();
if (!request.url().includes(urlPattern)) {
return false;
}
if (method && request.method() !== method) {
return false;
}
if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) {
const postData = request.postData();
if (postData) {
try {
const payload = JSON.parse(postData);
return payloadValidator(payload);
} catch {
return false;
}
}
}
return true;
},
{ timeout: 60 * 1000 },
);
} catch (error) {
if (error instanceof Error && error.message.includes("Target page, context or browser has been closed")) {
console.warn("Page was closed while waiting for API response:", urlPattern);
throw new Error(`Page closed while waiting for API response: ${urlPattern}`);
}
throw error;
}
}
export async function interceptAndInspectApiRequest(
page: Page,
urlPattern: string,
method: string = "POST",
error: Error,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorValidator: (url?: string, payload?: any) => boolean,
): Promise<void> {
await page.route(
(url) => url.pathname.includes(urlPattern),
async (route, request) => {
if (request.method() !== method) {
await route.continue();
return;
}
const postData = request.postData();
if (postData) {
try {
const payload = JSON.parse(postData);
if (errorValidator && errorValidator(request.url(), payload)) {
await route.fulfill({
status: 409,
contentType: "application/json",
body: JSON.stringify({
code: "Conflict",
message: error.message,
}),
});
return;
}
} catch (err) {
if (err instanceof Error && err.message.includes("not allowed")) {
throw err;
}
}
}
await route.continue();
},
);
}
export class ContainerCopy {
constructor(
public frame: Frame,
public wrapper: Locator,
) {}
static async waitForContainerCopy(page: Page): Promise<ContainerCopy> {
const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true });
const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper");
return new ContainerCopy(explorerFrame.frame, containerCopyWrapper);
}
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<ContainerCopy> {
const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true });
await page.goto(url);
return ContainerCopy.waitForContainerCopy(page);
}
}

View File

@@ -9,7 +9,8 @@ test("Gremlin graph CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.Gremlin); const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
// Create new database and graph // Create new database and graph
await explorer.globalCommandButton("New Graph").click(); const newGraphButton = await explorer.globalCommandButton("New Graph");
await newGraphButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Graph", "New Graph",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -14,7 +14,8 @@ import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUnique
const explorer = await DataExplorer.open(page, accountType); const explorer = await DataExplorer.open(page, accountType);
await explorer.globalCommandButton("New Collection").click(); const newCollectionButton = await explorer.globalCommandButton("New Collection");
await newCollectionButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Collection", "New Collection",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -8,7 +8,8 @@ test("SQL database and container CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.SQL); const explorer = await DataExplorer.open(page, TestAccount.SQL);
await explorer.globalCommandButton("New Container").click(); const newContainerButton = await explorer.globalCommandButton("New Container");
await newContainerButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Container", "New Container",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -0,0 +1,493 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect, Frame, Locator, Page, test } from "@playwright/test";
import { set } from "lodash";
import { truncateName } from "../../src/Explorer/ContainerCopy/CopyJobUtils";
import {
ContainerCopy,
getAccountName,
getDropdownItemByNameOrPosition,
interceptAndInspectApiRequest,
TestAccount,
waitForApiResponse,
} from "../fx";
import { createMultipleTestContainers } from "../testData";
let page: Page;
let wrapper: Locator = null!;
let panel: Locator = null!;
let frame: Frame = null!;
let expectedCopyJobNameInitial: string = null!;
let expectedJobName: string = "";
let targetAccountName: string = "";
let expectedSourceAccountName: string = "";
let expectedSubscriptionName: string = "";
const VISIBLE_TIMEOUT_MS = 30 * 1000;
test.describe.configure({ mode: "serial" });
test.describe("Container Copy", () => {
test.beforeAll("Container Copy - Before All", async ({ browser }) => {
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 3 });
page = await browser.newPage();
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
expectedJobName = `test_job_${Date.now()}`;
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
});
test.afterEach("Container Copy - After Each", async () => {
await page.unroute(/.*/, (route) => route.continue());
});
test("Loading and verifying the content of the page", async () => {
expect(wrapper).not.toBeNull();
await expect(wrapper.getByTestId("CommandBar/Button:Create Copy Job")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
});
test("Successfully create a copy job for offline migration", async () => {
expect(wrapper).not.toBeNull();
// Loading and verifying subscription & account dropdown
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
await createCopyJobButton.click();
panel = frame.getByTestId("Panel:Create copy job");
await expect(panel).toBeVisible();
await page.waitForTimeout(10 * 1000);
const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
const expectedAccountName = targetAccountName;
expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText();
await subscriptionDropdown.click();
const subscriptionItem = await getDropdownItemByNameOrPosition(
frame,
{ name: expectedSubscriptionName },
{ ariaLabel: "Subscription" },
);
await subscriptionItem.click();
// Load account dropdown based on selected subscription
const accountDropdown = panel.getByTestId("account-dropdown");
await expect(accountDropdown).toHaveText(new RegExp(expectedAccountName));
await accountDropdown.click();
const accountItem = await getDropdownItemByNameOrPosition(
frame,
{ name: expectedAccountName },
{ ariaLabel: "Account" },
);
await accountItem.click();
// Verifying online or offline checkbox functionality
/**
* This test verifies the functionality of the migration type checkbox that toggles between
* online and offline container copy modes. It ensures that:
* 1. When online mode is selected, the user is directed to a permissions screen
* 2. When offline mode is selected, the user bypasses the permissions screen
* 3. The UI correctly reflects the selected migration type throughout the workflow
*/
const fluentUiCheckboxContainer = panel.getByTestId("migration-type-checkbox").locator("div.ms-Checkbox");
await fluentUiCheckboxContainer.click();
await panel.getByRole("button", { name: "Next" }).click();
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).toBeVisible();
await expect(panel.getByText("Online container copy", { exact: true })).toBeVisible();
await panel.getByRole("button", { name: "Previous" }).click();
await fluentUiCheckboxContainer.click();
await panel.getByRole("button", { name: "Next" }).click();
await expect(panel.getByTestId("Panel:SelectSourceAndTargetContainers")).toBeVisible();
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).not.toBeVisible();
// Verifying source and target container selection
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
expect(sourceContainerDropdown).toBeVisible();
await expect(sourceContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
await sourceDatabaseDropdown.click();
const sourceDbDropdownItem = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Database" },
);
await sourceDbDropdownItem.click();
await expect(sourceContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
await sourceContainerDropdown.click();
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Container" },
);
await sourceContainerDropdownItem.click();
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
expect(targetContainerDropdown).toBeVisible();
await expect(targetContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
await targetDatabaseDropdown.click();
const targetDbDropdownItem = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Database" },
);
await targetDbDropdownItem.click();
await expect(targetContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
await targetContainerDropdown.click();
const targetContainerDropdownItem1 = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Container" },
);
await targetContainerDropdownItem1.click();
await panel.getByRole("button", { name: "Next" }).click();
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
await expect(errorContainer).toBeVisible();
await expect(errorContainer).toHaveText(/Source and destination containers cannot be the same/i);
// Reselect target container to be different from source container
await targetContainerDropdown.click();
const targetContainerDropdownItem2 = await getDropdownItemByNameOrPosition(
frame,
{ position: 1 },
{ ariaLabel: "Container" },
);
await targetContainerDropdownItem2.click();
const selectedSourceDatabase = await sourceDatabaseDropdown.innerText();
const selectedSourceContainer = await sourceContainerDropdown.innerText();
const selectedTargetDatabase = await targetDatabaseDropdown.innerText();
const selectedTargetContainer = await targetContainerDropdown.innerText();
expectedCopyJobNameInitial = `${truncateName(selectedSourceDatabase)}.${truncateName(
selectedSourceContainer,
)}_${truncateName(selectedTargetDatabase)}.${truncateName(selectedTargetContainer)}`;
await panel.getByRole("button", { name: "Next" }).click();
await expect(errorContainer).not.toBeVisible();
await expect(panel.getByTestId("Panel:PreviewCopyJob")).toBeVisible();
// Verifying the preview of the copy job
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
await expect(previewContainer).toBeVisible();
await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(expectedSubscriptionName);
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedAccountName);
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial));
const primaryBtn = panel.getByRole("button", { name: "Copy", exact: true });
await expect(primaryBtn).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
await jobNameInput.fill("test job name");
await expect(primaryBtn).toHaveClass(/(^|\s)is-disabled(\s|$)/);
// Testing API request interception with duplicate job name
const duplicateJobName = "test-job-name-1";
await jobNameInput.fill(duplicateJobName);
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
const expectedErrorMessage = `Duplicate job name '${duplicateJobName}'`;
await interceptAndInspectApiRequest(
page,
`${expectedAccountName}/dataTransferJobs/${duplicateJobName}`,
"PUT",
new Error(expectedErrorMessage),
(url?: string) => url?.includes(duplicateJobName) ?? false,
);
let errorThrown = false;
try {
await copyButton.click();
await page.waitForTimeout(2000);
} catch (error: any) {
errorThrown = true;
expect(error.message).toContain("not allowed");
}
if (!errorThrown) {
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
await expect(errorContainer).toBeVisible();
await expect(errorContainer).toHaveText(new RegExp(expectedErrorMessage, "i"));
}
await expect(panel).toBeVisible();
// Testing API request success with valid job name and verifying copy job creation
const validJobName = expectedJobName;
const copyJobCreationPromise = waitForApiResponse(
page,
`${expectedAccountName}/dataTransferJobs/${validJobName}`,
"PUT",
);
await jobNameInput.fill(validJobName);
await expect(copyButton).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
await copyButton.click();
const response = await copyJobCreationPromise;
expect(response.ok()).toBe(true);
await expect(panel).not.toBeVisible({ timeout: 10000 });
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
await jobsListContainer.waitFor({ state: "visible" });
const jobItem = jobsListContainer.getByText(validJobName);
await jobItem.waitFor({ state: "visible" });
await expect(jobItem).toBeVisible();
});
test("Verify Online or Offline Container Copy Permissions Panel", async () => {
expect(wrapper).not.toBeNull();
// Opening the Create Copy Job panel again to verify initial state
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
await createCopyJobButton.click();
panel = frame.getByTestId("Panel:Create copy job");
await expect(panel).toBeVisible();
await expect(panel.getByRole("heading", { name: "Create copy job" })).toBeVisible();
// select different account dropdown
const accountDropdown = panel.getByTestId("account-dropdown");
await accountDropdown.click();
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual("Account");
const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all();
const filteredItems = [];
for (const item of allDropdownItems) {
const testContent = (await item.textContent()) ?? "";
if (testContent.trim() !== targetAccountName.trim()) {
filteredItems.push(item);
}
}
if (filteredItems.length > 0) {
const firstDropdownItem = filteredItems[0];
expectedSourceAccountName = (await firstDropdownItem.textContent()) ?? "";
await firstDropdownItem.click();
} else {
throw new Error("No dropdown items available after filtering");
}
const fluentUiCheckboxContainer = panel.getByTestId("migration-type-checkbox").locator("div.ms-Checkbox");
await fluentUiCheckboxContainer.click();
await panel.getByRole("button", { name: "Next" }).click();
// Verifying Assign Permissions panel for online copy
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
await expect(permissionScreen).toBeVisible();
await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible();
await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible();
// Verify Point-in-Time Restore timer and refresh button workflow
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}**`, async (route) => {
const mockData = {
identity: {
type: "SystemAssigned",
principalId: "00-11-22-33",
},
properties: {
defaultIdentity: "SystemAssignedIdentity",
backupPolicy: {
type: "Continuous",
},
capabilities: [{ name: "EnableOnlineContainerCopy" }],
},
};
if (route.request().method() === "GET") {
const response = await route.fetch();
const actualData = await response.json();
const mergedData = { ...actualData };
set(mergedData, "identity", mockData.identity);
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mergedData),
});
} else {
await route.continue();
}
});
await expect(permissionScreen).toBeVisible();
const expandedOnlineAccordionHeader = permissionScreen
.getByTestId("permission-group-container-onlineConfigs")
.locator("button[aria-expanded='true']");
await expect(expandedOnlineAccordionHeader).toBeVisible();
const accordionItem = expandedOnlineAccordionHeader
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
.first();
const accordionPanel = accordionItem
.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']")
.first();
await page.clock.install({ time: new Date("2024-01-01T10:00:00Z") });
const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn");
await expect(pitrBtn).toBeVisible();
await pitrBtn.click();
page.context().on("page", async (newPage) => {
const expectedUrlEndPattern = new RegExp(
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${expectedSourceAccountName}/backupRestore`,
);
expect(newPage.url()).toMatch(expectedUrlEndPattern);
await newPage.close();
});
const loadingOverlay = frame.locator("[data-test='loading-overlay']");
await expect(loadingOverlay).toBeVisible();
const refreshBtn = accordionPanel.getByTestId("pointInTimeRestore:RefreshBtn");
await expect(refreshBtn).not.toBeVisible();
// Fast forward time by 11 minutes (11 * 60 * 1000ms = 660000ms)
await page.clock.fastForward(11 * 60 * 1000);
await expect(refreshBtn).toBeVisible();
await expect(pitrBtn).not.toBeVisible();
// Veify Popover & Loading Overlay on permission screen with API mocks and accordion interactions
await page.route(
`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/sqlRoleAssignments*`,
async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
value: [
{
principalId: "00-11-22-33",
roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/77-88-99`,
},
],
}),
});
},
);
await page.route("**/Microsoft.DocumentDB/databaseAccounts/*/77-88-99**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
value: [
{
name: "00000000-0000-0000-0000-000000000001",
},
],
}),
});
});
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}**`, async (route) => {
const mockData = {
identity: {
type: "SystemAssigned",
principalId: "00-11-22-33",
},
properties: {
defaultIdentity: "SystemAssignedIdentity",
backupPolicy: {
type: "Continuous",
},
capabilities: [{ name: "EnableOnlineContainerCopy" }],
},
};
if (route.request().method() === "PATCH") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ status: "Succeeded" }),
});
} else if (route.request().method() === "GET") {
// Get the actual response and merge with mock data
const response = await route.fetch();
const actualData = await response.json();
const mergedData = { ...actualData };
set(mergedData, "identity", mockData.identity);
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mergedData),
});
} else {
await route.continue();
}
});
await expect(permissionScreen).toBeVisible();
const expandedCrossAccordionHeader = permissionScreen
.getByTestId("permission-group-container-crossAccountConfigs")
.locator("button[aria-expanded='true']");
await expect(expandedCrossAccordionHeader).toBeVisible();
const crossAccordionItem = expandedCrossAccordionHeader
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
.first();
const crossAccordionPanel = crossAccordionItem
.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']")
.first();
const toggleButton = crossAccordionPanel.getByTestId("btn-toggle");
await expect(toggleButton).toBeVisible();
await toggleButton.click();
const popover = frame.locator("[data-test='popover-container']");
await expect(popover).toBeVisible();
const yesButton = popover.getByRole("button", { name: /Yes/i });
const noButton = popover.getByRole("button", { name: /No/i });
await expect(yesButton).toBeVisible();
await expect(noButton).toBeVisible();
await yesButton.click();
await expect(loadingOverlay).toBeVisible();
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
await expect(popover).toBeHidden({ timeout: 10 * 1000 });
await panel.getByRole("button", { name: "Cancel" }).click();
});
test.afterAll("Container Copy - After All", async () => {
await page.unroute(/.*/, (route) => route.continue());
await page.close();
});
});

View File

@@ -1,7 +1,18 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, DocumentsTab, TestAccount } from "../fx"; import { existsSync, mkdtempSync, rmdirSync, unlinkSync, writeFileSync } from "fs";
import { retry, setPartitionKeys } from "../testData"; import { tmpdir } from "os";
import path from "path";
import { CommandBarButton, DataExplorer, DocumentsTab, ONE_MINUTE_MS, TestAccount } from "../fx";
import {
createTestSQLContainer,
itemsPerPartition,
partitionCount,
retry,
setPartitionKeys,
TestContainerContext,
TestData,
} from "../testData";
import { documentTestCases } from "./testCases"; import { documentTestCases } from "./testCases";
let explorer: DataExplorer = null!; let explorer: DataExplorer = null!;
@@ -95,3 +106,105 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
} }
}); });
} }
test.describe.serial("Upload Item", () => {
let context: TestContainerContext = null!;
let uploadDocumentDirPath: string = null!;
let uploadDocumentFilePath: string = null!;
test.beforeAll("Create Test database and open documents tab", async ({ browser }) => {
uploadDocumentDirPath = mkdtempSync(path.join(tmpdir(), "upload-document-"));
uploadDocumentFilePath = path.join(uploadDocumentDirPath, "uploadDocument.json");
const page = await browser.newPage();
context = await createTestSQLContainer();
explorer = await DataExplorer.open(page, TestAccount.SQL);
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
const containerMenuNode = await explorer.waitForContainerItemsNode(context.database.id, context.container.id);
await containerMenuNode.element.click();
// We need to click twice in order to remove a tooltip
await containerMenuNode.element.click();
});
test.afterAll("Delete Test Database and uploadDocument temp folder", async () => {
if (existsSync(uploadDocumentFilePath)) {
unlinkSync(uploadDocumentFilePath);
}
if (existsSync(uploadDocumentDirPath)) {
rmdirSync(uploadDocumentDirPath);
}
if (!process.env.CI) {
await context?.dispose();
}
});
test.afterEach("Close Upload Items panel if still open", async () => {
const closeUploadItemsPanelButton = explorer.frame.getByLabel("Close Upload Items");
if (await closeUploadItemsPanelButton.isVisible()) {
await closeUploadItemsPanelButton.click();
}
});
test("upload document", async () => {
// Create file to upload
const TestDataJsonString: string = JSON.stringify(TestData, null, 2);
writeFileSync(uploadDocumentFilePath, TestDataJsonString);
const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
await uploadItemCommandBar.click();
// Select file to upload
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
await uploadButton.click();
// Verify upload success message
const fileUploadStatusExpected: string = `${partitionCount * itemsPerPartition} created, 0 throttled, 0 errors`;
const fileUploadStatus = explorer.frame.getByTestId("file-upload-status");
await expect(fileUploadStatus).toContainText(fileUploadStatusExpected, {
timeout: ONE_MINUTE_MS,
});
// Select file to upload again
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
await uploadButton.click();
// Verify upload failure message
const errorIcon = explorer.frame.getByRole("img", { name: "error" });
await expect(errorIcon).toBeVisible({ timeout: ONE_MINUTE_MS });
await expect(fileUploadStatus).toContainText(
`0 created, 0 throttled, ${partitionCount * itemsPerPartition} errors`,
{
timeout: ONE_MINUTE_MS,
},
);
});
test("upload invalid json", async () => {
// Create file to upload
let TestDataJsonString: string = JSON.stringify(TestData, null, 2);
// Remove the first '[' so that it becomes invalid json
TestDataJsonString = TestDataJsonString.substring(1);
writeFileSync(uploadDocumentFilePath, TestDataJsonString);
const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
await uploadItemCommandBar.click();
// Select file to upload
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
await uploadButton.click();
// Verify upload failure message
const fileUploadErrorList = explorer.frame.getByLabel("error list");
// The parsing error will show up differently in different browsers so just check for the word "JSON"
await expect(fileUploadErrorList).toContainText("JSON", {
timeout: ONE_MINUTE_MS,
});
});
});

Some files were not shown because too many files have changed in this diff Show More