mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-11 13:38:46 +00:00
Compare commits
12 Commits
user/bchou
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1090b3aa5 | ||
|
|
92c8afd166 | ||
|
|
234e4181fc | ||
|
|
38823ac86f | ||
|
|
b71ea50972 | ||
|
|
e27cff0553 | ||
|
|
4ac8cd8fe4 | ||
|
|
53288dec6f | ||
|
|
258a6286e7 | ||
|
|
c8ebca6da4 | ||
|
|
6167f94bc3 | ||
|
|
be89c634f3 |
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@@ -164,8 +164,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
shardTotal: [16]
|
||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
shardTotal: [20]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js 18.x
|
||||
@@ -192,24 +192,29 @@ jobs:
|
||||
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
|
||||
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
# CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
# echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||
# echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
# MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
# echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||
# echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
# MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
# echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||
# echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
# MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
# echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||
# echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
|
||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
@@ -250,4 +255,4 @@ jobs:
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
retention-days: 14
|
||||
6
.github/workflows/cleanup.yml
vendored
6
.github/workflows/cleanup.yml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Once every hour
|
||||
- cron: "0 15 * * *"
|
||||
# Once every two hours
|
||||
- cron: "0 */2 * * *"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -36,4 +36,4 @@ jobs:
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: npm ci
|
||||
- run: node utils/cleanupDBs.js
|
||||
- run: node utils/cleanupDBs.js
|
||||
@@ -406,7 +406,11 @@ body {
|
||||
width: 440px;
|
||||
min-height: 565px;
|
||||
}
|
||||
|
||||
.dataExplorerLoaderforcopyJobs{
|
||||
width: 100%;
|
||||
min-height: 565px;
|
||||
right: 0;
|
||||
}
|
||||
.dataExplorerTabLoaderContainer {
|
||||
left: initial;
|
||||
top: initial;
|
||||
|
||||
@@ -218,6 +218,7 @@ a:focus {
|
||||
|
||||
.tabPanesContainer {
|
||||
overflow: auto !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -116,8 +116,8 @@
|
||||
"tinykeys": "2.1.0",
|
||||
"underscore": "1.12.1",
|
||||
"utility-types": "3.10.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"uuid": "9.0.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"zustand": "3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -125,7 +125,7 @@
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -10321,12 +10321,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
|
||||
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
|
||||
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.49.1"
|
||||
"playwright": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -30903,12 +30904,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
|
||||
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.1"
|
||||
"playwright-core": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -30921,10 +30923,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
|
||||
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
|
||||
@@ -11,8 +11,8 @@ export default defineConfig({
|
||||
reporter: process.env.CI ? "blob" : "html",
|
||||
timeout: 10 * 60 * 1000,
|
||||
use: {
|
||||
trace: "off",
|
||||
video: "off",
|
||||
trace: "retain-on-failure",
|
||||
video: "retain-on-failure",
|
||||
screenshot: "on",
|
||||
testIdAttribute: "data-test",
|
||||
contextOptions: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
|
||||
import { useThemeStore } from "hooks/useTheme";
|
||||
import React from "react";
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
@@ -7,15 +8,17 @@ interface LoadingOverlayProps {
|
||||
}
|
||||
|
||||
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
|
||||
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||
if (!isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
data-test="loading-overlay"
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: "rgba(255,255,255,0.9)",
|
||||
backgroundColor: isDarkMode ? "rgba(32, 31, 30, 0.9)" : "rgba(255,255,255,0.9)",
|
||||
zIndex: 9999,
|
||||
display: "flex",
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,3 +11,14 @@
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Override dark mode inherit for pagination icons */
|
||||
body.isDarkMode .pager-container .ms-Button .ms-Button-icon,
|
||||
body.isDarkMode .pager-container .ms-Button i {
|
||||
color: var(--colorBrandForeground1);
|
||||
}
|
||||
|
||||
body.isDarkMode .pager-container .ms-Button:disabled .ms-Button-icon,
|
||||
body.isDarkMode .pager-container .ms-Button:disabled i {
|
||||
color: var(--colorNeutralForegroundDisabled);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ const Pager: React.FC<PagerProps> = ({
|
||||
return (
|
||||
<div className={className || "pager-container"}>
|
||||
{showItemCount && (
|
||||
<Text>
|
||||
<Text className="themeText">
|
||||
Showing {startIndex + 1} - {endIndex} of {totalCount} items
|
||||
</Text>
|
||||
)}
|
||||
@@ -82,7 +82,7 @@ const Pager: React.FC<PagerProps> = ({
|
||||
disabled={disabled || currentPage === 1}
|
||||
styles={iconButtonStyles}
|
||||
/>
|
||||
<Text>
|
||||
<Text className="themeText">
|
||||
Page {currentPage} of {totalPages}
|
||||
</Text>
|
||||
<IconButton
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports[`LoadingOverlay should handle long labels properly 1`] = `
|
||||
<div
|
||||
class="ms-Overlay root-109"
|
||||
data-test="loading-overlay"
|
||||
>
|
||||
<div
|
||||
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`] = `
|
||||
<div
|
||||
class="ms-Overlay root-109"
|
||||
data-test="loading-overlay"
|
||||
>
|
||||
<div
|
||||
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`] = `
|
||||
<div
|
||||
class="ms-Overlay root-109"
|
||||
data-test="loading-overlay"
|
||||
>
|
||||
<div
|
||||
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`] = `
|
||||
<div
|
||||
class="ms-Overlay root-109"
|
||||
data-test="loading-overlay"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-111"
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SqlStoredProcedureCreateUpdateParameters,
|
||||
SqlStoredProcedureResource,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
@@ -20,6 +20,7 @@ export async function createStoredProcedure(
|
||||
): Promise<StoredProcedureDefinition & Resource> {
|
||||
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
|
||||
try {
|
||||
let resource: StoredProcedureDefinition & Resource;
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
@@ -60,14 +61,16 @@ export async function createStoredProcedure(
|
||||
storedProcedure.id,
|
||||
createSprocParams,
|
||||
);
|
||||
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
resource = rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
} else {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedures.create(storedProcedure);
|
||||
resource = response.resource;
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedures.create(storedProcedure);
|
||||
return response?.resource;
|
||||
logConsoleInfo(`Successfully created stored procedure ${storedProcedure.id}`);
|
||||
return resource;
|
||||
} catch (error) {
|
||||
handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`);
|
||||
throw error;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
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 { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function createTrigger(
|
||||
): Promise<TriggerDefinition | SqlTriggerResource> {
|
||||
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
|
||||
try {
|
||||
let resource: SqlTriggerResource | TriggerDefinition;
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
@@ -52,14 +53,16 @@ export async function createTrigger(
|
||||
trigger.id,
|
||||
createTriggerParams,
|
||||
);
|
||||
return rpResponse && rpResponse.properties?.resource;
|
||||
resource = rpResponse && rpResponse.properties?.resource;
|
||||
} else {
|
||||
const sdkResponse = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.triggers.create(trigger as unknown as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type
|
||||
resource = sdkResponse.resource;
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.triggers.create(trigger as unknown as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type
|
||||
return response.resource;
|
||||
logConsoleInfo(`Successfully created trigger ${trigger.id}`);
|
||||
return resource;
|
||||
} catch (error) {
|
||||
handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`);
|
||||
throw error;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SqlUserDefinedFunctionCreateUpdateParameters,
|
||||
SqlUserDefinedFunctionResource,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
@@ -20,6 +20,7 @@ export async function createUserDefinedFunction(
|
||||
): Promise<UserDefinedFunctionDefinition & Resource> {
|
||||
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
|
||||
try {
|
||||
let resource: UserDefinedFunctionDefinition & Resource;
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
@@ -60,14 +61,17 @@ export async function createUserDefinedFunction(
|
||||
userDefinedFunction.id,
|
||||
createUDFParams,
|
||||
);
|
||||
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
||||
return response?.resource;
|
||||
resource = rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||
} else {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
||||
resource = response.resource;
|
||||
}
|
||||
logConsoleInfo(`Successfully created user defined function ${userDefinedFunction.id}`);
|
||||
return resource;
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { configContext } from "ConfigContext";
|
||||
import { ApiType, userContext } from "UserContext";
|
||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
@@ -14,9 +15,12 @@ import {
|
||||
DataTransferJobFeedResults,
|
||||
DataTransferJobGetResults,
|
||||
} from "Utils/arm/generatedClients/dataTransferService/types";
|
||||
import { armRequest } from "Utils/arm/request";
|
||||
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
||||
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
||||
|
||||
export const DATA_TRANSFER_JOB_API_VERSION = "2025-05-01-preview";
|
||||
|
||||
export interface DataTransferParams {
|
||||
jobName: string;
|
||||
apiType: ApiType;
|
||||
@@ -33,26 +37,34 @@ export const getDataTransferJobs = async (
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<DataTransferJobGetResults[]> => {
|
||||
let dataTransferJobs: DataTransferJobGetResults[] = [];
|
||||
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
signal,
|
||||
);
|
||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
||||
while (dataTransferFeeds?.nextLink) {
|
||||
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
|
||||
headers: {
|
||||
Authorization: userContext.authorizationToken,
|
||||
},
|
||||
/**
|
||||
* The `nextLink` URL returned by the Cosmos DB SQL API pointed to an incorrect endpoint, causing timeouts.
|
||||
* (i.e: https://cdbmgmtprodby.documents.azure.com:450/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.DocumentDB/databaseAccounts/{account}/sql/dataTransferJobs?$top=100&$skiptoken=...)
|
||||
* We manipulate the URL by parsing it to extract the path and query parameters,
|
||||
* then construct the correct URL for the Azure Resource Manager (ARM) API.
|
||||
* This ensures that the request is made to the correct base URL (`configContext.ARM_ENDPOINT`),
|
||||
* which is required for ARM operations.
|
||||
*/
|
||||
const parsedUrl = new URL(dataTransferFeeds.nextLink);
|
||||
const nextUrlPath = parsedUrl.pathname + parsedUrl.search;
|
||||
dataTransferFeeds = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: nextUrlPath,
|
||||
method: "GET",
|
||||
apiVersion: DATA_TRANSFER_JOB_API_VERSION,
|
||||
});
|
||||
if (nextResponse.ok) {
|
||||
dataTransferFeeds = await nextResponse.json();
|
||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
dataTransferJobs.push(...(dataTransferFeeds?.value || []));
|
||||
}
|
||||
return dataTransferJobs;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function deleteStoredProcedure(
|
||||
} else {
|
||||
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
|
||||
}
|
||||
logConsoleProgress(`Successfully deleted stored procedure ${storedProcedureId}`);
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
|
||||
throw error;
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
|
||||
} else {
|
||||
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
|
||||
}
|
||||
logConsoleProgress(`Successfully deleted trigger ${triggerId}`);
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
|
||||
throw error;
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
|
||||
} else {
|
||||
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
|
||||
}
|
||||
logConsoleProgress(`Successfully deleted user defined function ${id}`);
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
|
||||
throw error;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||
@@ -30,6 +31,7 @@ jest.mock("../../../Common/Logger");
|
||||
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
|
||||
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
||||
jest.mock("../CopyJobUtils");
|
||||
jest.mock("../../../Common/dataAccess/dataTransfers");
|
||||
|
||||
describe("CopyJobActions", () => {
|
||||
beforeEach(() => {
|
||||
@@ -154,33 +156,31 @@ describe("CopyJobActions", () => {
|
||||
});
|
||||
|
||||
it("should fetch and format copy jobs successfully", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "01:30:45",
|
||||
source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "source-db",
|
||||
containerName: "source-container",
|
||||
},
|
||||
destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "target-db",
|
||||
containerName: "target-container",
|
||||
},
|
||||
const mockResponse = [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "01:30:45",
|
||||
source: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "source-db",
|
||||
containerName: "source-container",
|
||||
},
|
||||
destination: {
|
||||
component: "CosmosDBSql",
|
||||
databaseName: "target-db",
|
||||
containerName: "target-container",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
@@ -201,38 +201,36 @@ describe("CopyJobActions", () => {
|
||||
});
|
||||
|
||||
it("should filter jobs by CosmosDBSql component", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "sql-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "02:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
const mockResponse = [
|
||||
{
|
||||
properties: {
|
||||
jobName: "sql-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "02:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
jobName: "other-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "01:00:00",
|
||||
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
jobName: "other-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "01:00:00",
|
||||
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
@@ -247,38 +245,36 @@ describe("CopyJobActions", () => {
|
||||
});
|
||||
|
||||
it("should sort jobs by last updated time (newest first)", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "older-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "01:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
const mockResponse = [
|
||||
{
|
||||
properties: {
|
||||
jobName: "older-job",
|
||||
status: "Completed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 100,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "01:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
jobName: "newer-job",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "00:30:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
jobName: "newer-job",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "00:30:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
@@ -293,25 +289,23 @@ describe("CopyJobActions", () => {
|
||||
});
|
||||
|
||||
it("should calculate completion percentage correctly", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 75,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "01:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
const mockResponse = [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "InProgress",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 75,
|
||||
totalCount: 100,
|
||||
mode: "online",
|
||||
duration: "01:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
@@ -325,25 +319,23 @@ describe("CopyJobActions", () => {
|
||||
});
|
||||
|
||||
it("should handle zero total count gracefully", async () => {
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "Pending",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 0,
|
||||
totalCount: 0,
|
||||
mode: "online",
|
||||
duration: "00:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
const mockResponse = [
|
||||
{
|
||||
properties: {
|
||||
jobName: "job-1",
|
||||
status: "Pending",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 0,
|
||||
totalCount: 0,
|
||||
mode: "online",
|
||||
duration: "00:00:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
@@ -361,26 +353,24 @@ describe("CopyJobActions", () => {
|
||||
message: "Error message line 1\r\n\r\nError message line 2",
|
||||
code: "ErrorCode123",
|
||||
};
|
||||
const mockResponse = {
|
||||
value: [
|
||||
{
|
||||
properties: {
|
||||
jobName: "failed-job",
|
||||
status: "Failed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "00:30:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
error: mockError,
|
||||
},
|
||||
const mockResponse = [
|
||||
{
|
||||
properties: {
|
||||
jobName: "failed-job",
|
||||
status: "Failed",
|
||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||
processedCount: 50,
|
||||
totalCount: 100,
|
||||
mode: "offline",
|
||||
duration: "00:30:00",
|
||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||
error: mockError,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||
timestamp: 1704106800000,
|
||||
@@ -408,7 +398,7 @@ describe("CopyJobActions", () => {
|
||||
};
|
||||
(global as any).AbortController = jest.fn(() => mockAbortController);
|
||||
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ value: [] });
|
||||
(getDataTransferJobs as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
getCopyJobs();
|
||||
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
||||
@@ -418,9 +408,7 @@ describe("CopyJobActions", () => {
|
||||
});
|
||||
|
||||
it("should throw error for invalid response format", async () => {
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({
|
||||
value: "not-an-array",
|
||||
});
|
||||
(getDataTransferJobs as jest.Mock).mockResolvedValue("not-an-array");
|
||||
|
||||
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
|
||||
});
|
||||
@@ -430,7 +418,7 @@ describe("CopyJobActions", () => {
|
||||
message: "Aborted",
|
||||
content: JSON.stringify({ message: "signal is aborted without reason" }),
|
||||
};
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError);
|
||||
(getDataTransferJobs as jest.Mock).mockRejectedValue(abortError);
|
||||
|
||||
await expect(getCopyJobs()).rejects.toMatchObject({
|
||||
message: expect.stringContaining("Previous copy job request was cancelled."),
|
||||
@@ -439,7 +427,7 @@ describe("CopyJobActions", () => {
|
||||
|
||||
it("should handle generic errors", async () => {
|
||||
const genericError = new Error("Network error");
|
||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(genericError);
|
||||
(getDataTransferJobs as jest.Mock).mockRejectedValue(genericError);
|
||||
|
||||
await expect(getCopyJobs()).rejects.toThrow("Network error");
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
||||
import { logError } from "../../../Common/Logger";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import {
|
||||
cancel,
|
||||
complete,
|
||||
create,
|
||||
listByDatabaseAccount,
|
||||
pause,
|
||||
resume,
|
||||
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||
@@ -63,14 +63,8 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
||||
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
||||
userContext.databaseAccount?.id || "",
|
||||
);
|
||||
const response = await listByDatabaseAccount(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
copyJobsAbortController.signal,
|
||||
);
|
||||
const jobs = await getDataTransferJobs(subscriptionId, resourceGroup, accountName, copyJobsAbortController.signal);
|
||||
|
||||
const jobs = response.value || [];
|
||||
if (!Array.isArray(jobs)) {
|
||||
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("CopyJobCommandBar", () => {
|
||||
|
||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe("CopyJobCommandBar", () => {
|
||||
|
||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
|
||||
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
|
||||
});
|
||||
|
||||
@@ -175,11 +175,11 @@ describe("CopyJobCommandBar", () => {
|
||||
mockConvertButton.mockReturnValue([]);
|
||||
|
||||
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1, false);
|
||||
|
||||
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
|
||||
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2, false);
|
||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { useThemeStore } from "../../../hooks/useTheme";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
|
||||
import { getThemeTokens } from "../../Theme/ThemeUtil";
|
||||
import { ContainerCopyProps } from "../Types/CopyJobTypes";
|
||||
import { getCommandBarButtons } from "./Utils";
|
||||
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
const rootStyle = {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
||||
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer);
|
||||
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||
const themeTokens = getThemeTokens(isDarkMode);
|
||||
const backgroundColor = themeTokens.colorNeutralBackground1;
|
||||
|
||||
const rootStyle = {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer, isDarkMode);
|
||||
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
|
||||
|
||||
return (
|
||||
<div className="commandBarContainer">
|
||||
<div className="commandBarContainer" style={{ backgroundColor }}>
|
||||
<FluentCommandBar
|
||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||
styles={rootStyle}
|
||||
|
||||
@@ -50,7 +50,7 @@ describe("CommandBar Utils", () => {
|
||||
|
||||
describe("getCommandBarButtons", () => {
|
||||
it("should return an array of command button props", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
expect(buttons).toBeDefined();
|
||||
expect(Array.isArray(buttons)).toBe(true);
|
||||
@@ -58,7 +58,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should include create copy job button", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
const createButton = buttons[0];
|
||||
|
||||
expect(createButton).toBeDefined();
|
||||
@@ -70,7 +70,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should include refresh button", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
const refreshButton = buttons[1];
|
||||
|
||||
expect(refreshButton).toBeDefined();
|
||||
@@ -80,11 +80,11 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should include feedback button when platform is Portal", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
expect(buttons.length).toBe(3);
|
||||
expect(buttons.length).toBe(4);
|
||||
|
||||
const feedbackButton = buttons[2];
|
||||
const feedbackButton = buttons[3];
|
||||
expect(feedbackButton).toBeDefined();
|
||||
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
|
||||
expect(feedbackButton.tooltipText).toBe("Feedback");
|
||||
@@ -105,13 +105,13 @@ describe("CommandBar Utils", () => {
|
||||
}));
|
||||
|
||||
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
|
||||
const buttons = getCommandBarButtonsEmulator(mockExplorer);
|
||||
const buttons = getCommandBarButtonsEmulator(mockExplorer, false);
|
||||
|
||||
expect(buttons.length).toBe(2);
|
||||
expect(buttons.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should call openCreateCopyJobPanel when create button is clicked", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
const createButton = buttons[0];
|
||||
|
||||
createButton.onCommandClick({} as React.SyntheticEvent);
|
||||
@@ -121,7 +121,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should call refreshJobList when refresh button is clicked", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
const refreshButton = buttons[1];
|
||||
|
||||
refreshButton.onCommandClick({} as React.SyntheticEvent);
|
||||
@@ -130,8 +130,8 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const feedbackButton = buttons[2];
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
const feedbackButton = buttons[3];
|
||||
|
||||
feedbackButton.onCommandClick({} as React.SyntheticEvent);
|
||||
|
||||
@@ -139,7 +139,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should return buttons with correct icon sources", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
expect(buttons[0].iconSrc).toBeDefined();
|
||||
expect(buttons[0].iconAlt).toBe("Create Copy Job");
|
||||
@@ -148,7 +148,10 @@ describe("CommandBar Utils", () => {
|
||||
expect(buttons[1].iconAlt).toBe("Refresh");
|
||||
|
||||
expect(buttons[2].iconSrc).toBeDefined();
|
||||
expect(buttons[2].iconAlt).toBe("Feedback");
|
||||
expect(buttons[2].iconAlt).toBe("Dark Theme");
|
||||
|
||||
expect(buttons[3].iconSrc).toBeDefined();
|
||||
expect(buttons[3].iconAlt).toBe("Feedback");
|
||||
});
|
||||
|
||||
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
|
||||
@@ -157,14 +160,14 @@ describe("CommandBar Utils", () => {
|
||||
return selector(state);
|
||||
});
|
||||
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
const refreshButton = buttons[1];
|
||||
|
||||
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||
});
|
||||
|
||||
it("should set hasPopup to false for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.hasPopup).toBe(false);
|
||||
@@ -172,7 +175,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should set commandButtonLabel to undefined for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.commandButtonLabel).toBeUndefined();
|
||||
@@ -180,7 +183,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should respect disabled state when provided", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.disabled).toBe(false);
|
||||
@@ -188,7 +191,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should return CommandButtonComponentProps with all required properties", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons.forEach((button: CommandButtonComponentProps) => {
|
||||
expect(button).toHaveProperty("iconSrc");
|
||||
@@ -202,18 +205,19 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should maintain button order: create, refresh, feedback", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
it("should maintain button order: create, refresh, themeToggle, feedback", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
expect(buttons[0].tooltipText).toBe("Create Copy Job");
|
||||
expect(buttons[1].tooltipText).toBe("Refresh");
|
||||
expect(buttons[2].tooltipText).toBe("Feedback");
|
||||
expect(buttons[2].tooltipText).toBe("Dark Theme");
|
||||
expect(buttons[3].tooltipText).toBe("Feedback");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Button click handlers", () => {
|
||||
it("should execute click handlers without errors", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||
@@ -221,7 +225,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should call correct action for each button", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons[0].onCommandClick({} as React.SyntheticEvent);
|
||||
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
||||
@@ -229,14 +233,14 @@ describe("CommandBar Utils", () => {
|
||||
buttons[1].onCommandClick({} as React.SyntheticEvent);
|
||||
expect(mockRefreshJobList).toHaveBeenCalled();
|
||||
|
||||
buttons[2].onCommandClick({} as React.SyntheticEvent);
|
||||
buttons[3].onCommandClick({} as React.SyntheticEvent);
|
||||
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("should have aria labels for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.ariaLabel).toBeDefined();
|
||||
@@ -246,7 +250,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should have tooltip text for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.tooltipText).toBeDefined();
|
||||
@@ -256,7 +260,7 @@ describe("CommandBar Utils", () => {
|
||||
});
|
||||
|
||||
it("should have icon alt text for all buttons", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer);
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
expect(button.iconAlt).toBeDefined();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import AddIcon from "../../../../images/Add.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import MoonIcon from "../../../../images/MoonIcon.svg";
|
||||
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
||||
import SunIcon from "../../../../images/SunIcon.svg";
|
||||
import { configContext, Platform } from "../../../ConfigContext";
|
||||
import { useThemeStore } from "../../../hooks/useTheme";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as Actions from "../Actions/CopyJobActions";
|
||||
@@ -9,7 +12,7 @@ import ContainerCopyMessages from "../ContainerCopyMessages";
|
||||
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
||||
|
||||
function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
|
||||
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
|
||||
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
|
||||
const buttons: CopyJobCommandBarBtnType[] = [
|
||||
{
|
||||
@@ -26,7 +29,15 @@ function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
|
||||
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
||||
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
||||
},
|
||||
{
|
||||
key: "themeToggle",
|
||||
iconSrc: isDarkMode ? SunIcon : MoonIcon,
|
||||
label: isDarkMode ? "Light Theme" : "Dark Theme",
|
||||
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme",
|
||||
onClick: () => useThemeStore.getState().toggleTheme(),
|
||||
},
|
||||
];
|
||||
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
buttons.push({
|
||||
key: "feedback",
|
||||
@@ -54,6 +65,6 @@ function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProp
|
||||
};
|
||||
}
|
||||
|
||||
export function getCommandBarButtons(explorer: Explorer): CommandButtonComponentProps[] {
|
||||
return getCopyJobBtns(explorer).map(btnMapper);
|
||||
export function getCommandBarButtons(explorer: Explorer, isDarkMode: boolean): CommandButtonComponentProps[] {
|
||||
return getCopyJobBtns(explorer, isDarkMode).map(btnMapper);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { SidePanel } from "../../Explorer/Panes/PanelContainerComponent";
|
||||
import CopyJobCommandBar from "./CommandBar/CopyJobCommandBar";
|
||||
import "./containerCopyStyles.less";
|
||||
import { MonitorCopyJobsRefState } from "./MonitorCopyJobs/MonitorCopyJobRefState";
|
||||
@@ -17,7 +16,6 @@ const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
||||
<div id="containerCopyWrapper" className="flexContainer hideOverflows">
|
||||
<CopyJobCommandBar explorer={explorer} />
|
||||
<MonitorCopyJobs ref={monitorCopyJobsRef} explorer={explorer} />
|
||||
<SidePanel />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
|
||||
const managedIdentityTooltip = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.addManagedIdentity.tooltip.content}
|
||||
<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}
|
||||
</Link>
|
||||
</Text>
|
||||
@@ -26,7 +31,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
|
||||
return (
|
||||
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
<Text>
|
||||
<Text className="themeText">
|
||||
{ContainerCopyMessages.addManagedIdentity.description} 
|
||||
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer">
|
||||
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText}
|
||||
@@ -35,6 +40,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
<InfoTooltip content={managedIdentityTooltip} />
|
||||
</Text>
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={systemAssigned}
|
||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||
|
||||
@@ -13,7 +13,12 @@ import useToggle from "./hooks/useToggle";
|
||||
const TooltipContent = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
||||
<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}
|
||||
</Link>
|
||||
</Text>
|
||||
@@ -65,6 +70,7 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
||||
<InfoTooltip content={TooltipContent} />
|
||||
</Text>
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={readPermissionAssigned}
|
||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useCopyJobPrerequisitesCache } from "../../Utils/useCopyJobPrerequisite
|
||||
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||
|
||||
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">
|
||||
<Text className="accordionHeaderText" variant="medium">
|
||||
{title}
|
||||
@@ -25,13 +25,13 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Compo
|
||||
height={completed ? 20 : 24}
|
||||
/>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel aria-disabled={disabled} className="accordionPanel">
|
||||
<AccordionPanel aria-disabled={disabled} className="accordionPanel" data-test="accordion-panel">
|
||||
<Component />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
|
||||
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description, sections }) => {
|
||||
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, description, sections }) => {
|
||||
const [openItems, setOpenItems] = React.useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,11 +44,12 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description,
|
||||
|
||||
return (
|
||||
<Stack
|
||||
data-test={`permission-group-container-${id}`}
|
||||
tokens={{ childrenGap: 15 }}
|
||||
styles={{
|
||||
root: {
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e1e1e1",
|
||||
background: "var(--colorNeutralBackground2)",
|
||||
border: "1px solid var(--colorNeutralStroke1)",
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
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 }}>
|
||||
<Text variant="medium" style={{ fontWeight: 600 }}>
|
||||
<Text variant="medium" style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text variant="small" styles={{ root: { color: "#605E5C" } }}>
|
||||
<Text variant="small" styles={{ root: { color: "var(--colorNeutralForeground2)" } }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
@@ -99,8 +100,12 @@ const AssignPermissions = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}>
|
||||
<Text variant="medium">
|
||||
<Stack
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
className="assignPermissionsContainer"
|
||||
tokens={{ childrenGap: 20 }}
|
||||
>
|
||||
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
||||
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
||||
copyJobState?.source?.account?.name || "",
|
||||
|
||||
@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
|
||||
const managedIdentityTooltip = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content}
|
||||
<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}
|
||||
</Link>
|
||||
</Text>
|
||||
@@ -31,6 +36,7 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
<InfoTooltip content={managedIdentityTooltip} />
|
||||
</div>
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={defaultSystemAssigned}
|
||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||
|
||||
@@ -13,7 +13,12 @@ import InfoTooltip from "../Components/InfoTooltip";
|
||||
const tooltipContent = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}
|
||||
<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}
|
||||
</Link>
|
||||
</Text>
|
||||
@@ -127,6 +132,7 @@ const PointInTimeRestore: React.FC = () => {
|
||||
<Stack.Item>
|
||||
{showRefreshButton ? (
|
||||
<PrimaryButton
|
||||
data-test="pointInTimeRestore:RefreshBtn"
|
||||
className="fullWidth"
|
||||
text={ContainerCopyMessages.refreshButtonLabel}
|
||||
iconProps={{ iconName: "Refresh" }}
|
||||
@@ -134,6 +140,7 @@ const PointInTimeRestore: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
data-test="pointInTimeRestore:PrimaryBtn"
|
||||
className="fullWidth"
|
||||
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
||||
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
||||
class="ms-Stack addManagedIdentityContainer css-109"
|
||||
>
|
||||
<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 don’t have to store any credentials in code.
|
||||
|
||||
@@ -67,6 +67,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
||||
class="ms-Toggle-background pill-117"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle1"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -92,7 +93,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
||||
class="ms-Stack addManagedIdentityContainer css-109"
|
||||
>
|
||||
<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 don’t have to store any credentials in code.
|
||||
|
||||
@@ -154,6 +155,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
||||
class="ms-Toggle-background pill-121"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle11"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -173,10 +175,12 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground loading css-123"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<div
|
||||
class="ms-Overlay root-135"
|
||||
data-test="loading-overlay"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-137"
|
||||
@@ -192,13 +196,13 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="css-124"
|
||||
class="themeText css-124"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
Enable system assigned managed identity
|
||||
</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.
|
||||
</span>
|
||||
@@ -261,7 +265,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
||||
class="ms-Stack addManagedIdentityContainer css-109"
|
||||
>
|
||||
<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 don’t have to store any credentials in code.
|
||||
|
||||
@@ -323,6 +327,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
||||
class="ms-Toggle-background pill-121"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle3"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -342,16 +347,17 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
||||
</div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground css-123"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<span
|
||||
class="css-124"
|
||||
class="themeText css-124"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
Enable system assigned managed identity
|
||||
</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.
|
||||
</span>
|
||||
|
||||
@@ -41,6 +41,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
||||
class="ms-Toggle-background pill-115"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle17"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -103,6 +104,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
||||
class="ms-Toggle-background pill-115"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle16"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -165,6 +167,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
class="ms-Toggle-background pill-115"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle3"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -227,6 +230,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
class="ms-Toggle-background pill-119"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle1"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -314,6 +318,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
class="ms-Toggle-background pill-115"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle0"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -376,6 +381,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
class="ms-Toggle-background pill-115"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle2"
|
||||
role="switch"
|
||||
type="button"
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
@@ -15,6 +16,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-112"
|
||||
data-test="permission-group-container-testGroup"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-113"
|
||||
@@ -36,6 +38,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||
@@ -85,6 +88,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
||||
</div>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||
@@ -134,6 +138,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||
data-test="accordion-panel"
|
||||
>
|
||||
<div>
|
||||
Incomplete Component
|
||||
@@ -142,6 +147,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
||||
</div>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
@@ -212,6 +219,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-112"
|
||||
data-test="permission-group-container-testGroup"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-113"
|
||||
@@ -233,6 +241,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||
@@ -331,6 +341,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||
data-test="accordion-panel"
|
||||
>
|
||||
<div>
|
||||
Incomplete Component
|
||||
@@ -339,6 +350,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
</div>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
@@ -409,6 +422,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-112"
|
||||
data-test="permission-group-container-testGroup"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-113"
|
||||
@@ -430,6 +444,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||
@@ -528,6 +544,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||
data-test="accordion-panel"
|
||||
>
|
||||
<div>
|
||||
Incomplete Component
|
||||
@@ -536,6 +553,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
</div>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
@@ -606,6 +625,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-112"
|
||||
data-test="permission-group-container-testGroup"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-113"
|
||||
@@ -627,6 +647,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
aria-disabled="false"
|
||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||
data-test="accordion-panel"
|
||||
>
|
||||
<div>
|
||||
Incomplete Component
|
||||
@@ -733,6 +756,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
||||
</div>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
@@ -803,6 +828,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-112"
|
||||
data-test="permission-group-container-crossAccountConfigs"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-113"
|
||||
@@ -824,6 +850,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="ms-Stack css-112"
|
||||
data-test="permission-group-container-onlineConfigs"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-113"
|
||||
@@ -896,6 +924,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||
@@ -945,6 +974,7 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||
data-test="accordion-panel"
|
||||
>
|
||||
<div
|
||||
data-testid="online-copy-enabled"
|
||||
@@ -964,6 +994,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
@@ -975,6 +1006,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-112"
|
||||
data-test="permission-group-container-onlineConfigs"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-113"
|
||||
@@ -996,6 +1028,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||
@@ -1094,6 +1128,7 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||
data-test="accordion-panel"
|
||||
>
|
||||
<div
|
||||
data-testid="online-copy-enabled"
|
||||
@@ -1113,6 +1148,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
@@ -1124,6 +1160,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-112"
|
||||
data-test="permission-group-container-crossAccountConfigs"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack css-113"
|
||||
@@ -1145,6 +1182,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||
@@ -1194,6 +1232,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
||||
</div>
|
||||
<div
|
||||
class="fui-AccordionItem"
|
||||
data-test="accordion-item"
|
||||
>
|
||||
<div
|
||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||
@@ -1243,6 +1282,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||
data-test="accordion-panel"
|
||||
>
|
||||
<div
|
||||
data-testid="add-read-permission"
|
||||
@@ -1262,6 +1302,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
@@ -1283,6 +1324,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack assignPermissionsContainer css-109"
|
||||
data-test="Panel:AssignPermissionsContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
|
||||
@@ -41,6 +41,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
|
||||
class="ms-Toggle-background pill-115"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle14"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -103,6 +104,7 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
|
||||
class="ms-Toggle-background pill-115"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle15"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -165,6 +167,7 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
|
||||
class="ms-Toggle-background pill-119"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle10"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -256,6 +259,7 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
|
||||
class="ms-Toggle-background pill-115"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle0"
|
||||
role="switch"
|
||||
type="button"
|
||||
@@ -318,6 +322,7 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
|
||||
class="ms-Toggle-background pill-119"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="btn-toggle"
|
||||
id="Toggle7"
|
||||
role="switch"
|
||||
type="button"
|
||||
|
||||
@@ -56,6 +56,7 @@ exports[`PointInTimeRestore Initial Render should render correctly with default
|
||||
<button
|
||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||
data-is-focusable="true"
|
||||
data-test="pointInTimeRestore:PrimaryBtn"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
@@ -131,6 +132,7 @@ exports[`PointInTimeRestore Initial Render should render with empty account name
|
||||
<button
|
||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||
data-is-focusable="true"
|
||||
data-test="pointInTimeRestore:PrimaryBtn"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
@@ -161,6 +163,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
|
||||
>
|
||||
<div
|
||||
class="ms-Overlay root-123"
|
||||
data-test="loading-overlay"
|
||||
>
|
||||
<div
|
||||
class="ms-Spinner root-125"
|
||||
@@ -223,6 +226,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
|
||||
aria-disabled="true"
|
||||
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
|
||||
data-is-focusable="false"
|
||||
data-test="pointInTimeRestore:PrimaryBtn"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
@@ -301,6 +305,7 @@ exports[`PointInTimeRestore Snapshots should match snapshot with refresh button
|
||||
<button
|
||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||
data-is-focusable="true"
|
||||
data-test="pointInTimeRestore:RefreshBtn"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -19,9 +19,21 @@ const NavigationControls: React.FC<NavigationControlsProps> = ({
|
||||
isPreviousDisabled,
|
||||
}) => (
|
||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} />
|
||||
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} />
|
||||
<DefaultButton text="Cancel" onClick={onCancel} />
|
||||
<PrimaryButton
|
||||
data-test="copy-job-primary"
|
||||
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>
|
||||
);
|
||||
|
||||
|
||||
@@ -17,15 +17,16 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
|
||||
({ isLoading = false, title, children, onPrimary, onCancel }) => {
|
||||
return (
|
||||
<Stack
|
||||
data-test="popover-container"
|
||||
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
|
||||
tokens={{ childrenGap: 20 }}
|
||||
style={{ maxWidth: 450 }}
|
||||
>
|
||||
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
|
||||
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>
|
||||
<Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text>{children}</Text>
|
||||
<Text className="themeText">{children}</Text>
|
||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
|
||||
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
|
||||
|
||||
@@ -4,14 +4,15 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground css-109"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
style="font-weight: 600;"
|
||||
/>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
<div>
|
||||
Test content
|
||||
@@ -71,10 +72,11 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground css-109"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
Test Title
|
||||
@@ -133,10 +135,11 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground css-109"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
Test Title
|
||||
@@ -195,16 +198,17 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground css-109"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
This is a very long title that might cause layout issues or text wrapping in the popover component
|
||||
</span>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
<div>
|
||||
Test content
|
||||
@@ -266,16 +270,17 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground css-109"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
Test Title
|
||||
</span>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
<div>
|
||||
Test content
|
||||
@@ -335,16 +340,17 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground css-109"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
Test Title
|
||||
</span>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
@@ -409,16 +415,17 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground css-109"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
Custom Title
|
||||
</span>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
<div>
|
||||
Test content
|
||||
@@ -478,6 +485,7 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack popover-container foreground loading css-109"
|
||||
data-test="popover-container"
|
||||
style="max-width: 450px;"
|
||||
>
|
||||
<div
|
||||
@@ -485,13 +493,13 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
|
||||
data-testid="loading-overlay"
|
||||
/>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
style="font-weight: 600;"
|
||||
>
|
||||
Test Title
|
||||
</span>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
<div>
|
||||
Test content
|
||||
|
||||
@@ -41,7 +41,7 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
||||
return (
|
||||
<Stack className="addCollectionPanelWrapper">
|
||||
<Stack.Item className="addCollectionPanelHeader">
|
||||
<Text>{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
||||
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item className="addCollectionPanelBody">
|
||||
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
Select the properties for your container.
|
||||
</span>
|
||||
@@ -50,7 +50,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
Select the properties for your container.
|
||||
</span>
|
||||
@@ -91,7 +91,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
Select the properties for your container.
|
||||
</span>
|
||||
@@ -132,7 +132,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
>
|
||||
<span
|
||||
class="css-111"
|
||||
class="themeText css-111"
|
||||
>
|
||||
Select the properties for your container.
|
||||
</span>
|
||||
|
||||
@@ -22,6 +22,7 @@ const CreateCopyJobScreens: React.FC = () => {
|
||||
<Stack.Item className="createCopyJobScreensContent">
|
||||
{contextError && (
|
||||
<MessageBar
|
||||
data-test="Panel:ErrorContainer"
|
||||
className="createCopyJobErrorMessageBar"
|
||||
messageBarType={MessageBarType.blocked}
|
||||
isMultiline={false}
|
||||
|
||||
@@ -31,17 +31,21 @@ const PreviewCopyJob: React.FC = () => {
|
||||
}));
|
||||
};
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
|
||||
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
|
||||
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
|
||||
<TextField value={jobName} onChange={onJobNameChange} />
|
||||
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
||||
</FieldRow>
|
||||
<Stack>
|
||||
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||
<Text>{copyJobState.source?.subscription?.displayName}</Text>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||
<Text data-test="source-subscription-name" className="themeText">
|
||||
{copyJobState.source?.subscription?.displayName}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text>{copyJobState.source?.account?.name}</Text>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text data-test="source-account-name" className="themeText">
|
||||
{copyJobState.source?.account?.name}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<DetailsList
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -32,6 +33,7 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField84"
|
||||
type="text"
|
||||
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"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -59,12 +62,13 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</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`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -350,6 +355,7 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField96"
|
||||
type="text"
|
||||
value=""
|
||||
@@ -363,12 +369,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -377,12 +384,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</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`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -668,6 +677,7 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField0"
|
||||
type="text"
|
||||
value=""
|
||||
@@ -681,12 +691,13 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -695,12 +706,13 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</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`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -986,6 +999,7 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField60"
|
||||
type="text"
|
||||
value=""
|
||||
@@ -999,12 +1013,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</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
|
||||
</span>
|
||||
@@ -1013,12 +1028,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</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
|
||||
</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`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -1304,6 +1321,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField36"
|
||||
type="text"
|
||||
value=""
|
||||
@@ -1317,12 +1335,13 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -1331,7 +1350,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</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`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -1617,6 +1637,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField24"
|
||||
type="text"
|
||||
value=""
|
||||
@@ -1630,7 +1651,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</span>
|
||||
@@ -1639,12 +1660,13 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -1901,6 +1923,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
||||
exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -1930,6 +1953,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField72"
|
||||
type="text"
|
||||
value="online-migration-job"
|
||||
@@ -1943,12 +1967,13 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -1957,12 +1982,13 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -2219,6 +2245,7 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -2248,6 +2275,7 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField12"
|
||||
type="text"
|
||||
value="custom-job-name-123"
|
||||
@@ -2261,12 +2289,13 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -2275,12 +2304,13 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</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`] = `
|
||||
<div
|
||||
class="ms-Stack previewCopyJobContainer css-109"
|
||||
data-test="Panel:PreviewCopyJob"
|
||||
>
|
||||
<div
|
||||
class="ms-Stack flex-row css-110"
|
||||
@@ -2566,6 +2597,7 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
||||
<input
|
||||
aria-invalid="false"
|
||||
class="ms-TextField-field field-115"
|
||||
data-test="job-name-textfield"
|
||||
id="TextField48"
|
||||
type="text"
|
||||
value=""
|
||||
@@ -2579,12 +2611,13 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -2593,12 +2626,13 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
||||
class="ms-Stack css-124"
|
||||
>
|
||||
<span
|
||||
class="bold css-125"
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="css-125"
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
|
||||
@@ -9,7 +9,7 @@ import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { CopyJobContext } from "../../../../Context/CopyJobContext";
|
||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||
import { AccountDropdown } from "./AccountDropdown";
|
||||
import { AccountDropdown, normalizeAccountId } from "./AccountDropdown";
|
||||
|
||||
jest.mock("../../../../../../hooks/useDatabaseAccounts");
|
||||
jest.mock("../../../../../../UserContext", () => ({
|
||||
@@ -202,13 +202,16 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
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 () => {
|
||||
const userContextAccount = {
|
||||
...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;
|
||||
@@ -223,7 +226,10 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
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 () => {
|
||||
@@ -248,7 +254,16 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
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 () => {
|
||||
@@ -272,7 +287,7 @@ describe("AccountDropdown", () => {
|
||||
it("should normalize account ID for Portal platform", () => {
|
||||
const portalAccount = {
|
||||
...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;
|
||||
|
||||
@@ -12,7 +12,7 @@ import FieldRow from "../../Components/FieldRow";
|
||||
|
||||
interface AccountDropdownProps {}
|
||||
|
||||
const normalizeAccountId = (id: string) => {
|
||||
export const normalizeAccountId = (id: string = "") => {
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
|
||||
} else if (configContext.platform === Platform.Hosted) {
|
||||
@@ -27,7 +27,12 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
|
||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||
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) => {
|
||||
setCopyJobState((prevState) => {
|
||||
@@ -47,7 +52,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
useEffect(() => {
|
||||
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
||||
const currentAccountId = copyJobState?.source?.account?.id;
|
||||
const predefinedAccountId = userContext.databaseAccount?.id;
|
||||
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
|
||||
const selectedAccountId = currentAccountId || predefinedAccountId;
|
||||
|
||||
const targetAccount: DatabaseAccount | null =
|
||||
@@ -58,7 +63,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
|
||||
const accountOptions =
|
||||
sqlApiOnlyAccounts?.map((account) => ({
|
||||
key: normalizeAccountId(account.id),
|
||||
key: account.id,
|
||||
text: account.name,
|
||||
data: account,
|
||||
})) || [];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/display-name */
|
||||
import { Checkbox, Stack } from "@fluentui/react";
|
||||
import { Checkbox, ICheckboxStyles, Stack } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
|
||||
@@ -9,8 +9,25 @@ interface MigrationTypeCheckboxProps {
|
||||
onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
|
||||
}
|
||||
|
||||
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
|
||||
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
|
||||
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
|
||||
</Stack>
|
||||
));
|
||||
const checkboxStyles: ICheckboxStyles = {
|
||||
text: { color: "var(--colorNeutralForeground1)" },
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
|
||||
<div
|
||||
class="ms-Stack migrationTypeRow css-109"
|
||||
data-test="migration-type-checkbox"
|
||||
>
|
||||
<div
|
||||
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`] = `
|
||||
<div
|
||||
class="ms-Stack migrationTypeRow css-109"
|
||||
data-test="migration-type-checkbox"
|
||||
>
|
||||
<div
|
||||
class="ms-Checkbox is-enabled root-110"
|
||||
|
||||
@@ -21,7 +21,7 @@ const SelectAccount = React.memo(() => {
|
||||
|
||||
return (
|
||||
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
|
||||
<Text>{ContainerCopyMessages.selectAccountDescription}</Text>
|
||||
<Text className="themeText">{ContainerCopyMessages.selectAccountDescription}</Text>
|
||||
|
||||
<SubscriptionDropdown />
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
||||
data-test="Panel:SelectAccountContainer"
|
||||
>
|
||||
<span
|
||||
class="css-110"
|
||||
class="themeText css-110"
|
||||
>
|
||||
Please select a source account from which to copy.
|
||||
</span>
|
||||
|
||||
@@ -47,8 +47,12 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
||||
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
|
||||
|
||||
return (
|
||||
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
|
||||
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
||||
<Stack
|
||||
data-test="Panel:SelectSourceAndTargetContainers"
|
||||
className="selectSourceAndTargetContainers"
|
||||
tokens={{ childrenGap: 25 }}
|
||||
>
|
||||
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
||||
<DatabaseContainerSection
|
||||
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
||||
databaseOptions={sourceDatabaseOptions}
|
||||
@@ -59,6 +63,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
||||
selectedContainer={source?.containerId}
|
||||
containerDisabled={!source?.databaseId}
|
||||
containerOnChange={onDropdownChange("sourceContainer")}
|
||||
sectionType="source"
|
||||
/>
|
||||
<DatabaseContainerSection
|
||||
heading={ContainerCopyMessages.targetContainerSubHeading}
|
||||
@@ -71,6 +76,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
||||
containerDisabled={!target?.databaseId}
|
||||
containerOnChange={onDropdownChange("targetContainer")}
|
||||
handleOnDemandCreateContainer={showAddCollectionPanel}
|
||||
sectionType="target"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ describe("DatabaseContainerSection", () => {
|
||||
selectedContainer: "container1",
|
||||
containerDisabled: false,
|
||||
containerOnChange: mockContainerOnChange,
|
||||
sectionType: "source",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -292,6 +293,7 @@ describe("DatabaseContainerSection", () => {
|
||||
containerOptions: mockContainerOptions,
|
||||
selectedContainer: "container1",
|
||||
containerOnChange: mockContainerOnChange,
|
||||
sectionType: "source",
|
||||
};
|
||||
|
||||
render(<DatabaseContainerSection {...minimalProps} />);
|
||||
@@ -393,6 +395,7 @@ describe("DatabaseContainerSection", () => {
|
||||
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
|
||||
selectedContainer: "c1",
|
||||
containerOnChange: jest.fn(),
|
||||
sectionType: "source",
|
||||
};
|
||||
|
||||
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
|
||||
@@ -411,6 +414,7 @@ describe("DatabaseContainerSection", () => {
|
||||
containerDisabled: false,
|
||||
containerOnChange: jest.fn(),
|
||||
handleOnDemandCreateContainer: jest.fn(),
|
||||
sectionType: "target",
|
||||
};
|
||||
|
||||
const { container } = render(<DatabaseContainerSection {...fullProps} />);
|
||||
@@ -428,6 +432,7 @@ describe("DatabaseContainerSection", () => {
|
||||
selectedContainer: "container1",
|
||||
containerDisabled: true,
|
||||
containerOnChange: jest.fn(),
|
||||
sectionType: "target",
|
||||
};
|
||||
|
||||
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
|
||||
@@ -443,6 +448,7 @@ describe("DatabaseContainerSection", () => {
|
||||
containerOptions: [],
|
||||
selectedContainer: "",
|
||||
containerOnChange: jest.fn(),
|
||||
sectionType: "target",
|
||||
};
|
||||
|
||||
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);
|
||||
|
||||
@@ -15,6 +15,7 @@ export const DatabaseContainerSection = ({
|
||||
containerDisabled,
|
||||
containerOnChange,
|
||||
handleOnDemandCreateContainer,
|
||||
sectionType,
|
||||
}: DatabaseContainerSectionProps) => (
|
||||
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
|
||||
<label className="subHeading">{heading}</label>
|
||||
@@ -27,6 +28,7 @@ export const DatabaseContainerSection = ({
|
||||
disabled={!!databaseDisabled}
|
||||
selectedKey={selectedDatabase}
|
||||
onChange={databaseOnChange}
|
||||
data-test={`${sectionType}-databaseDropdown`}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
|
||||
@@ -39,9 +41,14 @@ export const DatabaseContainerSection = ({
|
||||
disabled={!!containerDisabled}
|
||||
selectedKey={selectedContainer}
|
||||
onChange={containerOnChange}
|
||||
data-test={`${sectionType}-containerDropdown`}
|
||||
/>
|
||||
{handleOnDemandCreateContainer && (
|
||||
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
|
||||
<ActionButton
|
||||
className="create-container-link-btn"
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
onClick={() => handleOnDemandCreateContainer()}
|
||||
>
|
||||
{ContainerCopyMessages.createContainerButtonLabel}
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
@@ -37,6 +37,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
|
||||
class="ms-Dropdown is-required dropdown-112"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="target-databaseDropdown"
|
||||
id="Dropdown98"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
@@ -94,6 +95,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
|
||||
class="ms-Dropdown is-required dropdown-112"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="target-containerDropdown"
|
||||
id="Dropdown99"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
@@ -182,6 +184,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
|
||||
class="ms-Dropdown is-disabled is-required dropdown-143"
|
||||
data-is-focusable="false"
|
||||
data-ktp-target="true"
|
||||
data-test="target-databaseDropdown"
|
||||
id="Dropdown103"
|
||||
role="combobox"
|
||||
tabindex="-1"
|
||||
@@ -239,6 +242,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
|
||||
class="ms-Dropdown is-disabled is-required dropdown-143"
|
||||
data-is-focusable="false"
|
||||
data-ktp-target="true"
|
||||
data-test="target-containerDropdown"
|
||||
id="Dropdown104"
|
||||
role="combobox"
|
||||
tabindex="-1"
|
||||
@@ -306,6 +310,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
|
||||
class="ms-Dropdown is-required dropdown-112"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="target-databaseDropdown"
|
||||
id="Dropdown105"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
@@ -363,6 +368,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
|
||||
class="ms-Dropdown is-required dropdown-112"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="target-containerDropdown"
|
||||
id="Dropdown106"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
@@ -430,6 +436,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
|
||||
class="ms-Dropdown is-required dropdown-112"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="source-databaseDropdown"
|
||||
id="Dropdown96"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
@@ -487,6 +494,7 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
|
||||
class="ms-Dropdown is-required dropdown-112"
|
||||
data-is-focusable="true"
|
||||
data-ktp-target="true"
|
||||
data-test="source-containerDropdown"
|
||||
id="Dropdown97"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
|
||||
@@ -83,6 +83,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
data-test={`CopyJobActionMenu/Button:${job.Name}`}
|
||||
role="button"
|
||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||
menuProps={{ items: getMenuItems() }}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
|
||||
import React, { memo } from "react";
|
||||
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType } from "../../Types/CopyJobTypes";
|
||||
@@ -63,6 +64,19 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
|
||||
};
|
||||
|
||||
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 = [
|
||||
{
|
||||
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">
|
||||
{job.Error ? (
|
||||
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
|
||||
<Text className="bold" style={sectionCss.headingText}>
|
||||
<Text className="bold themeText" style={sectionCss.headingText}>
|
||||
{ContainerCopyMessages.errorTitle}
|
||||
</Text>
|
||||
<Text as="pre" style={{ whiteSpace: "pre-wrap" }}>
|
||||
<Text as="pre" style={errorMessageStyle}>
|
||||
{job.Error.message}
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
@@ -88,16 +102,16 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
||||
<Stack.Item data-testid="selectedcollection-stack">
|
||||
<Stack tokens={{ childrenGap: 15 }}>
|
||||
<Stack.Item style={sectionCss.verticalAlign}>
|
||||
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
|
||||
<Text>{job.LastUpdatedTime}</Text>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
|
||||
<Text className="themeText">{job.LastUpdatedTime}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={sectionCss.verticalAlign}>
|
||||
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text>{job.Source?.remoteAccountName}</Text>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={sectionCss.verticalAlign}>
|
||||
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
||||
<Text>{job.Mode}</Text>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
||||
<Text className="themeText">{job.Mode}</Text>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
|
||||
@@ -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 React from "react";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
|
||||
const theme = getTheme();
|
||||
|
||||
const iconClass = mergeStyles({
|
||||
fontSize: "16px",
|
||||
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>> = {
|
||||
[CopyJobStatusType.Pending]: "Clock",
|
||||
[CopyJobStatusType.Paused]: "CirclePause",
|
||||
@@ -35,6 +19,17 @@ const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
||||
[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 {
|
||||
status: CopyJobStatusType;
|
||||
}
|
||||
@@ -47,19 +42,17 @@ const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo((
|
||||
CopyJobStatusType.InProgress,
|
||||
CopyJobStatusType.Partitioning,
|
||||
].includes(status);
|
||||
const iconColor = statusIconColors[status] || "var(--colorNeutralForeground2)";
|
||||
const iconStyle = mergeStyles(iconClass, { color: iconColor });
|
||||
|
||||
return (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
{isSpinnerStatus ? (
|
||||
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
|
||||
) : (
|
||||
<FontIcon
|
||||
aria-label={status}
|
||||
iconName={iconMap[status] || "UnknownSolid"}
|
||||
className={classNames[status] || classNames.unknown}
|
||||
/>
|
||||
<FontIcon aria-label={status} iconName={iconMap[status] || "UnknownSolid"} className={iconStyle} />
|
||||
)}
|
||||
<Text>{statusText}</Text>
|
||||
<Text className="themeText">{statusText}</Text>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import React, { useEffect } from "react";
|
||||
import Pager from "../../../../Common/Pager";
|
||||
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||
import { getThemeTokens } from "../../../Theme/ThemeUtil";
|
||||
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import { getColumns } from "./CopyJobColumns";
|
||||
@@ -26,13 +28,15 @@ interface CopyJobsListProps {
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: { height: "calc(100vh - 25em)" } as React.CSSProperties,
|
||||
container: { height: "100%" } as React.CSSProperties,
|
||||
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
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 [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
|
||||
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}>
|
||||
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
||||
<ShimmeredDetailsList
|
||||
className="CopyJobListContainer"
|
||||
onRenderRow={_onRenderRow}
|
||||
checkboxVisibility={2}
|
||||
columns={columns}
|
||||
@@ -87,11 +92,28 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
||||
enableShimmer={false}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
onRenderDetailsHeader={(props, defaultRender) => (
|
||||
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
|
||||
{defaultRender({ ...props })}
|
||||
</Sticky>
|
||||
)}
|
||||
onRenderDetailsHeader={(props, defaultRender) => {
|
||||
const bgColor = themeTokens.colorNeutralBackground3;
|
||||
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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ScrollablePane>
|
||||
</Stack.Item>
|
||||
|
||||
@@ -13,7 +13,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spin
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
@@ -33,7 +33,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with sp
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
@@ -53,7 +53,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
@@ -66,7 +66,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
@@ -74,7 +74,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Cancelled
|
||||
</span>
|
||||
@@ -87,7 +87,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-5";"
|
||||
@@ -95,7 +95,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Completed
|
||||
</span>
|
||||
@@ -108,7 +108,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
@@ -116,7 +116,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
@@ -129,7 +129,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-4";"
|
||||
@@ -137,7 +137,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
@@ -150,7 +150,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-11";"
|
||||
@@ -158,7 +158,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Paused
|
||||
</span>
|
||||
@@ -171,7 +171,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-2";"
|
||||
@@ -179,7 +179,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Queued
|
||||
</span>
|
||||
@@ -192,7 +192,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
>
|
||||
<i
|
||||
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"
|
||||
role="img"
|
||||
style="font-family: "FabricMDL2Icons-9";"
|
||||
@@ -200,7 +200,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
||||
|
||||
</i>
|
||||
<span
|
||||
class="css-112"
|
||||
class="themeText css-112"
|
||||
>
|
||||
Cancelled
|
||||
</span>
|
||||
|
||||
@@ -10,7 +10,7 @@ import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
|
||||
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
||||
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%" });
|
||||
|
||||
interface MonitorCopyJobsProps {
|
||||
@@ -57,7 +57,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
||||
|
||||
useEffect(() => {
|
||||
fetchJobs();
|
||||
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS);
|
||||
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [fetchJobs]);
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface DatabaseContainerSectionProps {
|
||||
containerDisabled?: boolean;
|
||||
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
|
||||
handleOnDemandCreateContainer?: () => void;
|
||||
sectionType: "source" | "target";
|
||||
}
|
||||
|
||||
export interface CopyJobContextState {
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
@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 {
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
color: var(--colorNeutralForeground1);
|
||||
|
||||
.centerContent {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -9,20 +33,30 @@
|
||||
.noCopyJobsMessage {
|
||||
font-weight: 600;
|
||||
margin: 0 auto;
|
||||
color: @FocusColor;
|
||||
color: var(--colorNeutralForeground2);
|
||||
}
|
||||
button.createCopyJobButton {
|
||||
color: @LinkColor;
|
||||
color: var(--colorBrandForeground1);
|
||||
}
|
||||
}
|
||||
}
|
||||
.createCopyJobScreensContainer {
|
||||
height: 100%;
|
||||
padding: 1em 1.5em;
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
color: var(--colorNeutralForeground1);
|
||||
|
||||
.pointInTimeRestoreContainer, .onlineCopyContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
|
||||
.accordionHeaderText {
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
|
||||
label {
|
||||
padding: 0;
|
||||
@@ -71,7 +105,7 @@
|
||||
}
|
||||
.foreground {
|
||||
z-index: 10;
|
||||
background-color: #f9f9f9;
|
||||
background-color: var(--colorNeutralBackground2);
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
transform: translate(0%, -9%);
|
||||
@@ -80,14 +114,40 @@
|
||||
.createCopyJobErrorMessageBar {
|
||||
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 {
|
||||
padding: 0;
|
||||
height: 25px;
|
||||
color: @LinkColor;
|
||||
color: var(--colorBrandForeground1);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Create collection panel */
|
||||
@@ -105,7 +165,6 @@
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
.ms-DetailsList {
|
||||
width: 100%;
|
||||
|
||||
@@ -114,33 +173,33 @@
|
||||
padding: @DefaultSpace 20px;
|
||||
font-weight: 600;
|
||||
font-size: @DefaultFontSize;
|
||||
color: @BaseHigh;
|
||||
background-color: @BaseLow;
|
||||
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
||||
color: var(--colorNeutralForeground1);
|
||||
background-color: var(--colorNeutralBackground2);
|
||||
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
||||
|
||||
&:hover {
|
||||
background-color: @BaseMediumLow;
|
||||
background-color: var(--colorNeutralBackground3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ms-DetailsRow {
|
||||
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
||||
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
||||
|
||||
&:hover {
|
||||
background-color: @BaseMediumLow;
|
||||
background-color: var(--colorNeutralBackground2);
|
||||
}
|
||||
|
||||
.ms-DetailsRow-cell {
|
||||
padding: @MediumSpace 20px;
|
||||
font-size: @DefaultFontSize;
|
||||
color: @BaseHigh;
|
||||
color: var(--colorNeutralForeground1);
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.jobNameLink {
|
||||
color: @LinkColor;
|
||||
color: var(--colorBrandForeground1);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -168,7 +227,7 @@
|
||||
}
|
||||
.ms-DetailsRow-cell {
|
||||
font-size: @DefaultFontSize;
|
||||
color: @BaseHigh;
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { IndexingPolicy } from "@azure/cosmos";
|
||||
import { act } from "@testing-library/react";
|
||||
import { AuthType } from "AuthType";
|
||||
import { shallow } from "enzyme";
|
||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||
import ko from "knockout";
|
||||
import React from "react";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
@@ -444,3 +447,49 @@ describe("SettingsComponent", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ThroughputBucketsComponent,
|
||||
ThroughputBucketsComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
@@ -73,7 +74,6 @@ import {
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure,
|
||||
} from "./SettingsUtils";
|
||||
|
||||
interface SettingsV2TabInfo {
|
||||
tab: SettingsV2TabTypes;
|
||||
content: JSX.Element;
|
||||
@@ -182,7 +182,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private totalThroughputUsed: number;
|
||||
private throughputBucketsEnabled: boolean;
|
||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||
|
||||
private unsubscribe: () => void;
|
||||
constructor(props: SettingsComponentProps) {
|
||||
super(props);
|
||||
|
||||
@@ -312,6 +312,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
if (this.isCollectionSettingsTab) {
|
||||
this.refreshIndexTransformationProgress();
|
||||
this.loadMongoIndexes();
|
||||
this.unsubscribe = useIndexingPolicyStore.subscribe(
|
||||
() => {
|
||||
this.refreshCollectionData();
|
||||
},
|
||||
(state) => state.indexingPolicies[this.collection?.id()],
|
||||
);
|
||||
this.refreshCollectionData();
|
||||
}
|
||||
|
||||
this.setBaseline();
|
||||
@@ -319,7 +326,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
componentDidUpdate(): void {
|
||||
if (this.props.settingsTab.isActive()) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
@@ -849,7 +860,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
{ name: "name_of_property", query: "query_to_compute_property" },
|
||||
] as DataModels.ComputedProperties;
|
||||
}
|
||||
|
||||
const throughputBuckets = this.offer?.throughputBuckets;
|
||||
|
||||
return {
|
||||
@@ -1009,10 +1019,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
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> => {
|
||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||
|
||||
if (
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isContainerPolicyDirty ||
|
||||
@@ -1252,7 +1283,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||
throughputError: this.state.throughputError,
|
||||
};
|
||||
|
||||
if (!this.isCollectionSettingsTab) {
|
||||
return (
|
||||
<div className="settingsV2MainContainer">
|
||||
|
||||
@@ -155,7 +155,12 @@ export class ComputedPropertiesComponent extends React.Component<
|
||||
</Link>
|
||||
  about how to define computed properties and how to use them.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as React from "react";
|
||||
import { MessageBar, MessageBarType } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||
import {
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||
import { isIndexTransforming } from "../../SettingsUtils";
|
||||
|
||||
export interface IndexingPolicyRefreshComponentProps {
|
||||
|
||||
@@ -187,7 +187,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
||||
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
||||
</Stack>
|
||||
<Stack tokens={{ childrenGap: 5 }}>
|
||||
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
|
||||
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
|
||||
<Text styles={textSubHeadingStyle1}>
|
||||
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
|
||||
@@ -199,6 +199,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
{!isReadOnly && (
|
||||
<>
|
||||
<MessageBar
|
||||
data-test="partition-key-warning"
|
||||
messageBarType={MessageBarType.warning}
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={darkThemeMessageBarStyles}
|
||||
@@ -220,6 +221,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
</Text>
|
||||
{configContext.platform !== Platform.Emulator && (
|
||||
<PrimaryButton
|
||||
data-test="change-partition-key-button"
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text="Change"
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
|
||||
@@ -302,8 +302,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
|
||||
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: GeospatialConfigType.Geography, text: "Geography" },
|
||||
{ key: GeospatialConfigType.Geometry, text: "Geometry" },
|
||||
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
|
||||
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
|
||||
];
|
||||
|
||||
private getGeoSpatialComponent = (): JSX.Element => (
|
||||
|
||||
@@ -103,7 +103,10 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
offText="Inactive"
|
||||
checked={bucket.maxThroughputPercentage !== 100}
|
||||
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>
|
||||
</Stack>
|
||||
))}
|
||||
|
||||
@@ -31,6 +31,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
|
||||
</Text>
|
||||
<div
|
||||
className="settingsV2Editor"
|
||||
data-test="computed-properties-editor"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -78,6 +78,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
data-test="partition-key-values"
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 5,
|
||||
@@ -108,6 +109,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
||||
</Stack>
|
||||
</Stack>
|
||||
<StyledMessageBar
|
||||
data-test="partition-key-warning"
|
||||
messageBarIconProps={
|
||||
{
|
||||
"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.
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
data-test="change-partition-key-button"
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
{
|
||||
@@ -237,6 +240,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
data-test="partition-key-values"
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 5,
|
||||
|
||||
@@ -167,10 +167,12 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "geography-option",
|
||||
"key": "Geography",
|
||||
"text": "Geography",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "geometry-option",
|
||||
"key": "Geometry",
|
||||
"text": "Geometry",
|
||||
},
|
||||
@@ -652,10 +654,12 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "geography-option",
|
||||
"key": "Geography",
|
||||
"text": "Geography",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "geometry-option",
|
||||
"key": "Geometry",
|
||||
"text": "Geometry",
|
||||
},
|
||||
@@ -1224,10 +1228,12 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "geography-option",
|
||||
"key": "Geography",
|
||||
"text": "Geography",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "geometry-option",
|
||||
"key": "Geometry",
|
||||
"text": "Geometry",
|
||||
},
|
||||
@@ -1760,10 +1766,12 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "geography-option",
|
||||
"key": "Geography",
|
||||
"text": "Geography",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "geometry-option",
|
||||
"key": "Geometry",
|
||||
"text": "Geometry",
|
||||
},
|
||||
@@ -2330,10 +2338,12 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
options={
|
||||
[
|
||||
{
|
||||
"ariaLabel": "geography-option",
|
||||
"key": "Geography",
|
||||
"text": "Geography",
|
||||
},
|
||||
{
|
||||
"ariaLabel": "geometry-option",
|
||||
"key": "Geometry",
|
||||
"text": "Geometry",
|
||||
},
|
||||
|
||||
@@ -153,6 +153,16 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKey",
|
||||
],
|
||||
"rawDataModel": {
|
||||
"indexingPolicy": {
|
||||
"automatic": true,
|
||||
"compositeIndexes": [],
|
||||
"excludedPaths": [],
|
||||
"fullTextIndexes": [],
|
||||
"includedPaths": [],
|
||||
"indexingMode": "consistent",
|
||||
"spatialIndexes": [],
|
||||
"vectorIndexes": [],
|
||||
},
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": [
|
||||
{
|
||||
@@ -264,6 +274,16 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKey",
|
||||
],
|
||||
"rawDataModel": {
|
||||
"indexingPolicy": {
|
||||
"automatic": true,
|
||||
"compositeIndexes": [],
|
||||
"excludedPaths": [],
|
||||
"fullTextIndexes": [],
|
||||
"includedPaths": [],
|
||||
"indexingMode": "consistent",
|
||||
"spatialIndexes": [],
|
||||
"vectorIndexes": [],
|
||||
},
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": [
|
||||
{
|
||||
@@ -476,6 +496,16 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKey",
|
||||
],
|
||||
"rawDataModel": {
|
||||
"indexingPolicy": {
|
||||
"automatic": true,
|
||||
"compositeIndexes": [],
|
||||
"excludedPaths": [],
|
||||
"fullTextIndexes": [],
|
||||
"includedPaths": [],
|
||||
"indexingMode": "consistent",
|
||||
"spatialIndexes": [],
|
||||
"vectorIndexes": [],
|
||||
},
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": [
|
||||
{
|
||||
@@ -653,6 +683,16 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKey",
|
||||
],
|
||||
"rawDataModel": {
|
||||
"indexingPolicy": {
|
||||
"automatic": true,
|
||||
"compositeIndexes": [],
|
||||
"excludedPaths": [],
|
||||
"fullTextIndexes": [],
|
||||
"includedPaths": [],
|
||||
"indexingMode": "consistent",
|
||||
"spatialIndexes": [],
|
||||
"vectorIndexes": [],
|
||||
},
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": [
|
||||
{
|
||||
|
||||
@@ -53,6 +53,7 @@ type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexTyp
|
||||
const labelStyles = {
|
||||
root: {
|
||||
fontSize: 12,
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,6 +64,8 @@ const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldSt
|
||||
field: {
|
||||
fontSize: 12,
|
||||
padding: "0 8px",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -437,13 +437,14 @@ export default class Explorer {
|
||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||
if (isFabricMirroredKey()) {
|
||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
||||
return;
|
||||
} else {
|
||||
await (userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases());
|
||||
await this.refreshNotebookList();
|
||||
}
|
||||
|
||||
await (userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases());
|
||||
await this.refreshNotebookList();
|
||||
logConsoleInfo("Successfully refreshed databases");
|
||||
};
|
||||
|
||||
// Facade
|
||||
|
||||
@@ -853,7 +853,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{!isSynapseLinkEnabled() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small">
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
Azure Synapse Link is required for creating an analytical store{" "}
|
||||
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
||||
<Link
|
||||
|
||||
@@ -475,6 +475,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
Azure Synapse Link is required for creating an analytical store
|
||||
|
||||
@@ -208,7 +208,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</div>
|
||||
</Stack>
|
||||
{createNewContainer ? (
|
||||
<Stack>
|
||||
<Stack data-test="create-new-container-form">
|
||||
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
@@ -230,6 +230,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
<input
|
||||
data-test="new-container-id-input"
|
||||
name="collectionId"
|
||||
id="collectionId"
|
||||
type="text"
|
||||
@@ -271,6 +272,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
|
||||
<input
|
||||
type="text"
|
||||
data-test="new-container-partition-key-input"
|
||||
id="addCollection-partitionKeyValue"
|
||||
aria-required
|
||||
required
|
||||
@@ -304,6 +306,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
type="text"
|
||||
id="addCollection-partitionKeyValue"
|
||||
key={`addCollection-partitionKeyValue_${index}`}
|
||||
data-test={`new-container-sub-partition-key-input-${index}`}
|
||||
aria-required
|
||||
required
|
||||
size={40}
|
||||
@@ -327,6 +330,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
data-test={`remove-sub-partition-key-button-${index}`}
|
||||
ariaLabel="Remove hierarchical partition key"
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27 }}
|
||||
onClick={() => {
|
||||
@@ -339,6 +344,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
})}
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
data-test="add-sub-partition-key-button"
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||
@@ -346,7 +352,11 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
Add hierarchical partition key
|
||||
</DefaultButton>
|
||||
{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
|
||||
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
|
||||
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
||||
@@ -359,7 +369,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack>
|
||||
<Stack data-test="use-existing-container-form">
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
@@ -390,6 +400,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
}}
|
||||
defaultSelectedKey={targetCollectionId}
|
||||
responsiveMode={999}
|
||||
ariaLabel="Existing Containers"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
||||
|
||||
export const PanelLoadingScreen: React.FunctionComponent = () => (
|
||||
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
|
||||
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerLoaderforcopyJobs">
|
||||
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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."
|
||||
/>
|
||||
{uploadFileData?.length > 0 && (
|
||||
<div className="fileUploadSummaryContainer">
|
||||
<div className="fileUploadSummaryContainer" data-test="file-upload-status">
|
||||
<b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b>
|
||||
<DetailsList
|
||||
items={uploadFileData}
|
||||
|
||||
107
src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.tsx
Normal file
107
src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -3,18 +3,21 @@ import QueryError from "Common/QueryError";
|
||||
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
|
||||
import { MessageBanner } from "Explorer/Controls/MessageBanner";
|
||||
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||
import useZoomLevel from "hooks/useZoomLevel";
|
||||
import React from "react";
|
||||
import { conditionalClass } from "Utils/StyleUtils";
|
||||
import RunQuery from "../../../../images/RunQuery.png";
|
||||
import { QueryResults } from "../../../Contracts/ViewModels";
|
||||
import { ErrorList } from "./ErrorList";
|
||||
import { ResultsView } from "./ResultsView";
|
||||
import useZoomLevel from "hooks/useZoomLevel";
|
||||
import { conditionalClass } from "Utils/StyleUtils";
|
||||
|
||||
export interface ResultsViewProps {
|
||||
isMongoDB: boolean;
|
||||
queryResults: QueryResults;
|
||||
executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>;
|
||||
queryEditorContent?: string;
|
||||
databaseId?: string;
|
||||
containerId?: string;
|
||||
}
|
||||
|
||||
interface QueryResultProps extends ResultsViewProps {
|
||||
@@ -49,6 +52,8 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
|
||||
queryResults,
|
||||
executeQueryDocumentsPage,
|
||||
isExecuting,
|
||||
databaseId,
|
||||
containerId,
|
||||
}: QueryResultProps): JSX.Element => {
|
||||
const styles = useQueryTabStyles();
|
||||
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
|
||||
@@ -91,6 +96,9 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
|
||||
queryResults={queryResults}
|
||||
executeQueryDocumentsPage={executeQueryDocumentsPage}
|
||||
isMongoDB={isMongoDB}
|
||||
queryEditorContent={queryEditorContent}
|
||||
databaseId={databaseId}
|
||||
containerId={containerId}
|
||||
/>
|
||||
) : (
|
||||
<ExecuteQueryCallToAction />
|
||||
|
||||
@@ -52,8 +52,9 @@ describe("QueryTabComponent", () => {
|
||||
copilotVersion: "v3.0",
|
||||
},
|
||||
});
|
||||
|
||||
const propsMock: Readonly<IQueryTabComponentProps> = {
|
||||
collection: { databaseId: "CopilotSampleDB" },
|
||||
collection: { databaseId: "CopilotSampleDB", id: () => "CopilotContainer" },
|
||||
onTabAccessor: () => jest.fn(),
|
||||
isExecutionError: false,
|
||||
tabId: "mockTabId",
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useMonacoTheme } from "hooks/useTheme";
|
||||
import React, { Fragment, createRef } from "react";
|
||||
import "react-splitter-layout/lib/index.css";
|
||||
import { format } from "react-string-format";
|
||||
import create from "zustand";
|
||||
//TODO: Uncomment next two lines when query copilot is reinstated in DE
|
||||
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||
@@ -57,6 +58,20 @@ import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
||||
import TabsBase from "../TabsBase";
|
||||
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 {
|
||||
Result,
|
||||
QueryMetrics,
|
||||
@@ -264,6 +279,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
setTimeout(async () => {
|
||||
@@ -780,6 +799,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
errors={this.props.copilotStore?.errors}
|
||||
isExecuting={this.props.copilotStore?.isExecuting}
|
||||
queryResults={this.props.copilotStore?.queryResults}
|
||||
databaseId={this.props.collection.databaseId}
|
||||
containerId={this.props.collection.id()}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||
QueryDocumentsPerPage(
|
||||
firstItemIndex,
|
||||
@@ -795,6 +816,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
errors={this.state.errors}
|
||||
isExecuting={this.state.isExecuting}
|
||||
queryResults={this.state.queryResults}
|
||||
databaseId={this.props.collection.databaseId}
|
||||
containerId={this.props.collection.id()}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||
this._executeQueryDocumentsPage(firstItemIndex)
|
||||
}
|
||||
|
||||
170
src/Explorer/Tabs/QueryTab/ResultsView.test.tsx
Normal file
170
src/Explorer/Tabs/QueryTab/ResultsView.test.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { CompositePath, IndexingPolicy } from "@azure/cosmos";
|
||||
import { FontIcon } from "@fluentui/react";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DataGrid,
|
||||
DataGridBody,
|
||||
DataGridCell,
|
||||
@@ -8,28 +11,45 @@ import {
|
||||
DataGridRow,
|
||||
SelectTabData,
|
||||
SelectTabEvent,
|
||||
Spinner,
|
||||
Tab,
|
||||
TabList,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumnDefinition,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
createTableColumn,
|
||||
} from "@fluentui/react-components";
|
||||
import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons";
|
||||
import { ArrowDownloadRegular, ChevronDown20Regular, ChevronRight20Regular, CopyRegular } from "@fluentui/react-icons";
|
||||
import copy from "clipboard-copy";
|
||||
import { HttpHeaders } from "Common/Constants";
|
||||
import MongoUtility from "Common/MongoUtility";
|
||||
import { QueryMetrics } from "Contracts/DataModels";
|
||||
import { QueryResults } from "Contracts/ViewModels";
|
||||
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 { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import copy from "clipboard-copy";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||
import create from "zustand";
|
||||
import { client } from "../../../Common/CosmosClient";
|
||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { sampleDataClient } from "../../../Common/SampleDataClient";
|
||||
import { ResultsViewProps } from "./QueryResultSection";
|
||||
|
||||
import { useIndexAdvisorStyles } from "./StylesAdvisor";
|
||||
enum ResultsTabs {
|
||||
Results = "results",
|
||||
QueryStats = "queryStats",
|
||||
IndexAdvisor = "indexadv",
|
||||
}
|
||||
|
||||
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
|
||||
const styles = useQueryTabStyles();
|
||||
/* eslint-disable react/prop-types */
|
||||
@@ -523,14 +543,331 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
|
||||
);
|
||||
};
|
||||
|
||||
export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
|
||||
export interface IIndexMetric {
|
||||
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 [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
|
||||
|
||||
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
|
||||
setActiveTab(data.value as ResultsTabs);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
|
||||
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
|
||||
@@ -548,6 +885,13 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
||||
>
|
||||
Query Stats
|
||||
</Tab>
|
||||
<Tab
|
||||
data-test="QueryTab/ResultsPane/ResultsView/IndexAdvisorTab"
|
||||
id={ResultsTabs.IndexAdvisor}
|
||||
value={ResultsTabs.IndexAdvisor}
|
||||
>
|
||||
Index Advisor
|
||||
</Tab>
|
||||
</TabList>
|
||||
<div className={styles.queryResultsTabContentContainer}>
|
||||
{activeTab === ResultsTabs.Results && (
|
||||
@@ -558,7 +902,30 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
||||
/>
|
||||
)}
|
||||
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
|
||||
{activeTab === ResultsTabs.IndexAdvisor && (
|
||||
<IndexAdvisorTab
|
||||
queryResults={queryResults}
|
||||
queryEditorContent={queryEditorContent}
|
||||
databaseId={databaseId}
|
||||
containerId={containerId}
|
||||
/>
|
||||
)}
|
||||
</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 },
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
95
src/Explorer/Tabs/QueryTab/StylesAdvisor.ts
Normal file
95
src/Explorer/Tabs/QueryTab/StylesAdvisor.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
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",
|
||||
},
|
||||
},
|
||||
});
|
||||
15
src/Explorer/Tabs/QueryTab/useQueryMetadataStore.ts
Normal file
15
src/Explorer/Tabs/QueryTab/useQueryMetadataStore.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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 }),
|
||||
}));
|
||||
@@ -435,7 +435,6 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
});
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}, 100);
|
||||
|
||||
return createdResource;
|
||||
},
|
||||
(createError) => {
|
||||
|
||||
192
src/Main.tsx
192
src/Main.tsx
@@ -2,9 +2,18 @@
|
||||
import "./ReactDevTools";
|
||||
|
||||
// CSS Dependencies
|
||||
import { initializeIcons } from "@fluentui/react";
|
||||
import { initializeIcons, loadTheme, useTheme } from "@fluentui/react";
|
||||
import { FluentProvider, makeStyles, webDarkTheme, webLightTheme } from "@fluentui/react-components";
|
||||
import { Platform } from "ConfigContext";
|
||||
import ContainerCopyPanel from "Explorer/ContainerCopy/ContainerCopyPanel";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||
import { userContext } from "UserContext";
|
||||
import "allotment/dist/style.css";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "../externals/jquery-ui.min.css";
|
||||
@@ -15,8 +24,13 @@ import "../externals/jquery.dataTables.min.css";
|
||||
import "../externals/jquery.typeahead.min.css";
|
||||
import "../externals/jquery.typeahead.min.js";
|
||||
// Image Dependencies
|
||||
import { SidePanel } from "Explorer/Panes/PanelContainerComponent";
|
||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||
import { SidebarContainer } from "Explorer/Sidebar";
|
||||
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
||||
import "allotment/dist/style.css";
|
||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import "../images/favicon.ico";
|
||||
import "../less/TableStyles/CustomizeColumns.less";
|
||||
import "../less/TableStyles/EntityEditor.less";
|
||||
@@ -28,29 +42,178 @@ import "../less/infobox.less";
|
||||
import "../less/menus.less";
|
||||
import "../less/messagebox.less";
|
||||
import "../less/resourceTree.less";
|
||||
import * as StyleConstants from "./Common/StyleConstants";
|
||||
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
||||
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
||||
import { Dialog } from "./Explorer/Controls/Dialog";
|
||||
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
||||
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
||||
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
||||
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
||||
import { ErrorBoundary } from "./Explorer/ErrorBoundary";
|
||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
||||
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
|
||||
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
||||
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
||||
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import "./Explorer/Panes/PanelComponent.less";
|
||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||
import "./Libs/jquery";
|
||||
import { MetricScenarioProvider } from "./Metrics/MetricScenarioProvider";
|
||||
import Root from "./RootComponents/Root";
|
||||
import MetricScenario from "./Metrics/MetricEvents";
|
||||
import { MetricScenarioProvider, useMetricScenario } from "./Metrics/MetricScenarioProvider";
|
||||
import { ApplicationMetricPhase } from "./Metrics/ScenarioConfig";
|
||||
import { useInteractive } from "./Metrics/useMetricPhases";
|
||||
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
||||
import "./Shared/appInsights";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||
import { useThemeStore } from "./hooks/useTheme";
|
||||
import "./less/DarkModeMenus.less";
|
||||
import "./less/ThemeSystem.less";
|
||||
|
||||
// Initialize icons before React is loaded
|
||||
initializeIcons(undefined, { disableWarnings: true });
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
height: "100vh",
|
||||
width: "100vw",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
},
|
||||
});
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
const config = useConfig();
|
||||
const styles = useStyles();
|
||||
// theme is used for application-wide styling
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const theme = useTheme();
|
||||
|
||||
// Load Fabric theme and styles only once when platform is Fabric
|
||||
React.useEffect(() => {
|
||||
if (config?.platform === Platform.Fabric) {
|
||||
loadTheme(appThemeFabric);
|
||||
import("../less/documentDBFabric.less");
|
||||
}
|
||||
StyleConstants.updateStyles();
|
||||
}, [config?.platform]);
|
||||
|
||||
const explorer = useKnockoutExplorer(config?.platform);
|
||||
|
||||
// Scenario-based health tracking: start ApplicationLoad and complete phases.
|
||||
const { startScenario, completePhase } = useMetricScenario();
|
||||
React.useEffect(() => {
|
||||
startScenario(MetricScenario.ApplicationLoad);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (explorer) {
|
||||
completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [explorer]);
|
||||
|
||||
if (!explorer) {
|
||||
return <LoadingExplorer />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Main" className={styles.root}>
|
||||
<KeyboardShortcutRoot>
|
||||
<div className="flexContainer" aria-hidden="false">
|
||||
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
|
||||
<>
|
||||
<ContainerCopyPanel explorer={explorer} />
|
||||
<SidePanel />
|
||||
</>
|
||||
) : (
|
||||
<DivExplorer explorer={explorer} />
|
||||
)}
|
||||
</div>
|
||||
</KeyboardShortcutRoot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DivExplorer: React.FC<{ explorer: Explorer }> = ({ explorer }) => {
|
||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||
useInteractive(MetricScenario.ApplicationLoad);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flexContainer"
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
data-test="DataExplorerRoot"
|
||||
>
|
||||
<div
|
||||
id="divExplorer"
|
||||
className="flexContainer hideOverflows"
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
}}
|
||||
>
|
||||
<div id="freeTierTeachingBubble"> </div>
|
||||
<CommandBar container={explorer} />
|
||||
<SidebarContainer explorer={explorer} />
|
||||
<div
|
||||
className="dataExplorerErrorConsoleContainer"
|
||||
role="contentinfo"
|
||||
aria-label="Notification console"
|
||||
id="explorerNotificationConsole"
|
||||
style={{
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
}}
|
||||
>
|
||||
<NotificationConsole />
|
||||
</div>
|
||||
</div>
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
{<SQLQuickstartTutorial />}
|
||||
{<MongoQuickstartTutorial />}
|
||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Root: React.FC = () => {
|
||||
// Use React state to track isDarkMode and subscribe to changes
|
||||
const [isDarkMode, setIsDarkMode] = React.useState(useThemeStore.getState().isDarkMode);
|
||||
const currentTheme = isDarkMode ? webDarkTheme : webLightTheme;
|
||||
|
||||
// Subscribe to theme changes
|
||||
React.useEffect(() => {
|
||||
return useThemeStore.subscribe((state) => {
|
||||
setIsDarkMode(state.isDarkMode);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<FluentProvider theme={currentTheme}>
|
||||
<App />
|
||||
</FluentProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const mainElement = document.getElementById("Main");
|
||||
if (mainElement) {
|
||||
ReactDOM.render(
|
||||
@@ -60,3 +223,24 @@ if (mainElement) {
|
||||
mainElement,
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingExplorer(): JSX.Element {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="splashLoaderContainer">
|
||||
<div className="splashLoaderContentContainer">
|
||||
<p className="connectExplorerContent">
|
||||
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
||||
</p>
|
||||
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
||||
Welcome to Azure Cosmos DB
|
||||
</p>
|
||||
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
||||
Connecting...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import MetricScenario from "./MetricEvents";
|
||||
import { MetricPhase } from "./ScenarioConfig";
|
||||
import { scenarioMonitor } from "./ScenarioMonitor";
|
||||
|
||||
export interface MetricScenarioContextValue {
|
||||
interface MetricScenarioContextValue {
|
||||
startScenario: (scenario: MetricScenario) => void;
|
||||
startPhase: (scenario: MetricScenario, phase: MetricPhase) => void;
|
||||
completePhase: (scenario: MetricScenario, phase: MetricPhase) => void;
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
import { loadTheme } from "@fluentui/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { updateStyles } from "../Common/StyleConstants";
|
||||
import { Platform } from "../ConfigContext";
|
||||
import { useConfig } from "../hooks/useConfig";
|
||||
import { useKnockoutExplorer } from "../hooks/useKnockoutExplorer";
|
||||
import { MetricScenarioContextValue, useMetricScenario } from "../Metrics/MetricScenarioProvider";
|
||||
import App from "./App";
|
||||
|
||||
const mockUserContext = {
|
||||
features: { enableContainerCopy: false },
|
||||
apiType: "SQL",
|
||||
};
|
||||
|
||||
jest.mock("@fluentui/react", () => ({
|
||||
loadTheme: jest.fn(),
|
||||
makeStyles: jest.fn(() => () => ({
|
||||
root: "mock-app-root-class",
|
||||
})),
|
||||
MessageBarType: {
|
||||
error: "error",
|
||||
warning: "warning",
|
||||
info: "info",
|
||||
success: "success",
|
||||
},
|
||||
SpinnerSize: {
|
||||
xSmall: "xSmall",
|
||||
small: "small",
|
||||
medium: "medium",
|
||||
large: "large",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../Common/StyleConstants", () => ({
|
||||
StyleConstants: {
|
||||
BaseMedium: "#000000",
|
||||
AccentMediumHigh: "#0078d4",
|
||||
AccentMedium: "#106ebe",
|
||||
AccentLight: "#deecf9",
|
||||
AccentAccentExtra: "#0078d4",
|
||||
FabricAccentMediumHigh: "#0078d4",
|
||||
FabricAccentMedium: "#106ebe",
|
||||
FabricAccentLight: "#deecf9",
|
||||
PortalAccentMediumHigh: "#0078d4",
|
||||
PortalAccentMedium: "#106ebe",
|
||||
PortalAccentLight: "#deecf9",
|
||||
},
|
||||
updateStyles: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("./LoadingExplorer", () => {
|
||||
const MockLoadingExplorer = () => {
|
||||
return <div data-testid="mock-loading-explorer">Loading Explorer</div>;
|
||||
};
|
||||
MockLoadingExplorer.displayName = "MockLoadingExplorer";
|
||||
return MockLoadingExplorer;
|
||||
});
|
||||
|
||||
jest.mock("./ExplorerContainer", () => {
|
||||
const MockExplorerContainer = ({ explorer }: { explorer: unknown }) => {
|
||||
return (
|
||||
<div data-testid="mock-explorer-container">Explorer Container - {explorer ? "with explorer" : "no explorer"}</div>
|
||||
);
|
||||
};
|
||||
MockExplorerContainer.displayName = "MockExplorerContainer";
|
||||
return MockExplorerContainer;
|
||||
});
|
||||
|
||||
jest.mock("../Explorer/ContainerCopy/ContainerCopyPanel", () => {
|
||||
const MockContainerCopyPanel = ({ explorer }: { explorer: unknown }) => {
|
||||
return (
|
||||
<div data-testid="mock-container-copy-panel">
|
||||
Container Copy Panel - {explorer ? "with explorer" : "no explorer"}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
MockContainerCopyPanel.displayName = "MockContainerCopyPanel";
|
||||
return MockContainerCopyPanel;
|
||||
});
|
||||
|
||||
jest.mock("../KeyboardShortcuts", () => ({
|
||||
KeyboardShortcutRoot: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="mock-keyboard-shortcut-root">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("../UserContext", () => ({
|
||||
get userContext() {
|
||||
return mockUserContext;
|
||||
},
|
||||
}));
|
||||
|
||||
const mockConfig = {
|
||||
platform: Platform.Portal,
|
||||
};
|
||||
|
||||
const mockExplorer = {
|
||||
id: "test-explorer",
|
||||
name: "Test Explorer",
|
||||
};
|
||||
|
||||
jest.mock("../hooks/useConfig", () => ({
|
||||
useConfig: jest.fn(() => mockConfig),
|
||||
}));
|
||||
|
||||
jest.mock("../hooks/useKnockoutExplorer", () => ({
|
||||
useKnockoutExplorer: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../Metrics/MetricScenarioProvider", () => ({
|
||||
useMetricScenario: jest.fn(() => ({
|
||||
startScenario: jest.fn(),
|
||||
completePhase: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock("../Metrics/MetricEvents", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
ApplicationLoad: "ApplicationLoad",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../Metrics/ScenarioConfig", () => ({
|
||||
ApplicationMetricPhase: {
|
||||
ExplorerInitialized: "ExplorerInitialized",
|
||||
},
|
||||
CommonMetricPhase: {
|
||||
Interactive: "Interactive",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../Platform/Fabric/FabricTheme", () => ({
|
||||
appThemeFabric: { name: "fabric-theme" },
|
||||
}));
|
||||
|
||||
describe("App", () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUserContext.features = { enableContainerCopy: false };
|
||||
mockUserContext.apiType = "SQL";
|
||||
});
|
||||
let mockStartScenario: jest.Mock;
|
||||
let mockCompletePhase: jest.Mock;
|
||||
let mockUseKnockoutExplorer: jest.Mock;
|
||||
let mockUseConfig: jest.Mock;
|
||||
let mockLoadTheme: jest.Mock;
|
||||
let mockUpdateStyles: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockStartScenario = jest.fn();
|
||||
mockCompletePhase = jest.fn();
|
||||
|
||||
mockUseKnockoutExplorer = jest.mocked(useKnockoutExplorer);
|
||||
mockUseConfig = jest.mocked(useConfig);
|
||||
mockLoadTheme = jest.mocked(loadTheme);
|
||||
mockUpdateStyles = jest.mocked(updateStyles);
|
||||
|
||||
const mockUseMetricScenario = jest.mocked(useMetricScenario);
|
||||
mockUseMetricScenario.mockReturnValue({
|
||||
startScenario: mockStartScenario,
|
||||
completePhase: mockCompletePhase
|
||||
} as unknown as MetricScenarioContextValue);
|
||||
|
||||
mockUseConfig.mockReturnValue(mockConfig);
|
||||
mockUseKnockoutExplorer.mockReturnValue(null);
|
||||
});
|
||||
|
||||
test("should render loading explorer when explorer is not ready", () => {
|
||||
mockUseKnockoutExplorer.mockReturnValue(null);
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByTestId("mock-loading-explorer")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("mock-explorer-container")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should render explorer container when explorer is ready", () => {
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByTestId("mock-explorer-container")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("mock-loading-explorer")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should start metric scenario on mount", () => {
|
||||
render(<App />);
|
||||
|
||||
expect(mockStartScenario).toHaveBeenCalledWith("ApplicationLoad");
|
||||
expect(mockStartScenario).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should complete metric phase when explorer is initialized", async () => {
|
||||
const { rerender } = render(<App />);
|
||||
|
||||
expect(mockCompletePhase).not.toHaveBeenCalled();
|
||||
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
rerender(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCompletePhase).toHaveBeenCalledWith("ApplicationLoad", "ExplorerInitialized");
|
||||
});
|
||||
});
|
||||
|
||||
test("should load fabric theme when platform is Fabric", () => {
|
||||
const fabricConfig = { platform: Platform.Fabric };
|
||||
mockUseConfig.mockReturnValue(fabricConfig);
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(mockLoadTheme).toHaveBeenCalledWith({ name: "fabric-theme" });
|
||||
});
|
||||
|
||||
test("should not load fabric theme when platform is not Fabric", () => {
|
||||
const portalConfig = { platform: Platform.Portal };
|
||||
mockUseConfig.mockReturnValue(portalConfig);
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(mockLoadTheme).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should always call updateStyles", () => {
|
||||
render(<App />);
|
||||
|
||||
expect(mockUpdateStyles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should render container copy panel when container copy is enabled and API is SQL", () => {
|
||||
mockUserContext.features = { enableContainerCopy: true };
|
||||
mockUserContext.apiType = "SQL";
|
||||
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByTestId("mock-container-copy-panel")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("mock-explorer-container")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should render explorer container when container copy is disabled", () => {
|
||||
mockUserContext.features = { enableContainerCopy: false };
|
||||
mockUserContext.apiType = "SQL";
|
||||
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByTestId("mock-explorer-container")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("mock-container-copy-panel")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should render explorer container when API is not SQL", () => {
|
||||
mockUserContext.features = { enableContainerCopy: true };
|
||||
mockUserContext.apiType = "MongoDB";
|
||||
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByTestId("mock-explorer-container")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("mock-container-copy-panel")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should have correct DOM structure", () => {
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
|
||||
const { container } = render(<App />);
|
||||
|
||||
const mainDiv = container.querySelector("#Main");
|
||||
expect(mainDiv).toBeInTheDocument();
|
||||
expect(mainDiv).toHaveClass("mock-app-root-class");
|
||||
|
||||
expect(screen.getByTestId("mock-keyboard-shortcut-root")).toBeInTheDocument();
|
||||
|
||||
const flexContainer = container.querySelector(".flexContainer");
|
||||
expect(flexContainer).toBeInTheDocument();
|
||||
expect(flexContainer).toHaveAttribute("aria-hidden", "false");
|
||||
});
|
||||
|
||||
test("should handle config changes for Fabric platform", () => {
|
||||
const { rerender } = render(<App />);
|
||||
|
||||
const fabricConfig = { platform: Platform.Fabric };
|
||||
mockUseConfig.mockReturnValue(fabricConfig);
|
||||
|
||||
rerender(<App />);
|
||||
|
||||
expect(mockLoadTheme).toHaveBeenCalledWith({ name: "fabric-theme" });
|
||||
});
|
||||
|
||||
test("should pass explorer to child components", () => {
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByText("Explorer Container - with explorer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should handle null config gracefully", () => {
|
||||
mockUseConfig.mockReturnValue(null);
|
||||
mockUseKnockoutExplorer.mockReturnValue(mockExplorer);
|
||||
|
||||
expect(() => render(<App />)).not.toThrow();
|
||||
|
||||
expect(mockLoadTheme).not.toHaveBeenCalled();
|
||||
expect(mockUpdateStyles).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { loadTheme, makeStyles } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import * as StyleConstants from "../Common/StyleConstants";
|
||||
import { Platform } from "../ConfigContext";
|
||||
import ContainerCopyPanel from "../Explorer/ContainerCopy/ContainerCopyPanel";
|
||||
import { useConfig } from "../hooks/useConfig";
|
||||
import { useKnockoutExplorer } from "../hooks/useKnockoutExplorer";
|
||||
import { KeyboardShortcutRoot } from "../KeyboardShortcuts";
|
||||
import MetricScenario from "../Metrics/MetricEvents";
|
||||
import { useMetricScenario } from "../Metrics/MetricScenarioProvider";
|
||||
import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig";
|
||||
import { appThemeFabric } from "../Platform/Fabric/FabricTheme";
|
||||
import { userContext } from "../UserContext";
|
||||
import ExplorerContainer from "./ExplorerContainer";
|
||||
import LoadingExplorer from "./LoadingExplorer";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
height: "100vh",
|
||||
width: "100vw",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
},
|
||||
});
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
const config = useConfig();
|
||||
const styles = useStyles();
|
||||
// Load Fabric theme and styles only once when platform is Fabric
|
||||
React.useEffect(() => {
|
||||
if (config?.platform === Platform.Fabric) {
|
||||
loadTheme(appThemeFabric);
|
||||
import("../../less/documentDBFabric.less");
|
||||
}
|
||||
StyleConstants.updateStyles();
|
||||
}, [config?.platform]);
|
||||
|
||||
const explorer = useKnockoutExplorer(config?.platform);
|
||||
|
||||
// Scenario-based health tracking: start ApplicationLoad and complete phases.
|
||||
const { startScenario, completePhase } = useMetricScenario();
|
||||
React.useEffect(() => {
|
||||
startScenario(MetricScenario.ApplicationLoad);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (explorer) {
|
||||
completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [explorer]);
|
||||
|
||||
if (!explorer) {
|
||||
return <LoadingExplorer />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Main" className={styles.root}>
|
||||
<KeyboardShortcutRoot>
|
||||
<div className="flexContainer" aria-hidden="false">
|
||||
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
|
||||
<ContainerCopyPanel explorer={explorer} />
|
||||
) : (
|
||||
<ExplorerContainer explorer={explorer} />
|
||||
)}
|
||||
</div>
|
||||
</KeyboardShortcutRoot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -1,183 +0,0 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { useCarousel } from "../hooks/useCarousel";
|
||||
import { useInteractive } from "../Metrics/useMetricPhases";
|
||||
import ExplorerContainer from "./ExplorerContainer";
|
||||
|
||||
jest.mock("../Explorer/Controls/Dialog", () => ({
|
||||
Dialog: () => <div data-testid="mock-dialog">Dialog</div>,
|
||||
}));
|
||||
|
||||
jest.mock("../Explorer/Menus/CommandBar/CommandBarComponentAdapter", () => ({
|
||||
CommandBar: ({ container }: { container: Explorer }) => (
|
||||
<div data-testid="mock-command-bar">CommandBar - {container ? "with explorer" : "no explorer"}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("../Explorer/Menus/NotificationConsole/NotificationConsoleComponent", () => ({
|
||||
NotificationConsole: () => <div data-testid="mock-notification-console">NotificationConsole</div>,
|
||||
}));
|
||||
|
||||
jest.mock("../Explorer/Panes/PanelContainerComponent", () => ({
|
||||
SidePanel: () => <div data-testid="mock-side-panel">SidePanel</div>,
|
||||
}));
|
||||
|
||||
jest.mock("../Explorer/QueryCopilot/CopilotCarousel", () => ({
|
||||
QueryCopilotCarousel: ({ isOpen, explorer }: { isOpen: boolean; explorer: Explorer }) => (
|
||||
<div data-testid="mock-copilot-carousel">
|
||||
CopilotCarousel - {isOpen ? "open" : "closed"} - {explorer ? "with explorer" : "no explorer"}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("../Explorer/Quickstart/QuickstartCarousel", () => ({
|
||||
QuickstartCarousel: ({ isOpen }: { isOpen: boolean }) => (
|
||||
<div data-testid="mock-quickstart-carousel">QuickstartCarousel - {isOpen ? "open" : "closed"}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("../Explorer/Quickstart/Tutorials/MongoQuickstartTutorial", () => ({
|
||||
MongoQuickstartTutorial: () => <div data-testid="mock-mongo-tutorial">MongoQuickstartTutorial</div>,
|
||||
}));
|
||||
|
||||
jest.mock("../Explorer/Quickstart/Tutorials/SQLQuickstartTutorial", () => ({
|
||||
SQLQuickstartTutorial: () => <div data-testid="mock-sql-tutorial">SQLQuickstartTutorial</div>,
|
||||
}));
|
||||
|
||||
jest.mock("../Explorer/Sidebar", () => ({
|
||||
SidebarContainer: ({ explorer }: { explorer: Explorer }) => (
|
||||
<div data-testid="mock-sidebar-container">SidebarContainer - {explorer ? "with explorer" : "no explorer"}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("../hooks/useCarousel", () => ({
|
||||
useCarousel: jest.fn((selector) => {
|
||||
if (selector.toString().includes("shouldOpen")) {
|
||||
return true;
|
||||
}
|
||||
if (selector.toString().includes("showCopilotCarousel")) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../Metrics/useMetricPhases", () => ({
|
||||
useInteractive: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../Metrics/MetricEvents", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
ApplicationLoad: "ApplicationLoad",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("ExplorerContainer", () => {
|
||||
let mockExplorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer = {
|
||||
id: "test-explorer",
|
||||
name: "Test Explorer",
|
||||
} as unknown as Explorer;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should render explorer container with all components", () => {
|
||||
const { container } = render(<ExplorerContainer explorer={mockExplorer} />);
|
||||
|
||||
const mainContainer = container.querySelector('[data-test="DataExplorerRoot"]');
|
||||
expect(mainContainer).toBeInTheDocument();
|
||||
expect(mainContainer).toHaveClass("flexContainer");
|
||||
|
||||
expect(screen.getByTestId("mock-command-bar")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-sidebar-container")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-notification-console")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-side-panel")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-dialog")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-quickstart-carousel")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-sql-tutorial")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-mongo-tutorial")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-copilot-carousel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should pass explorer to components that need it", () => {
|
||||
render(<ExplorerContainer explorer={mockExplorer} />);
|
||||
|
||||
expect(screen.getByText("CommandBar - with explorer")).toBeInTheDocument();
|
||||
expect(screen.getByText("SidebarContainer - with explorer")).toBeInTheDocument();
|
||||
expect(screen.getByText("CopilotCarousel - closed - with explorer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should have correct DOM structure", () => {
|
||||
const { container } = render(<ExplorerContainer explorer={mockExplorer} />);
|
||||
|
||||
const mainContainer = container.querySelector('[data-test="DataExplorerRoot"]');
|
||||
expect(mainContainer).toBeInTheDocument();
|
||||
expect(mainContainer).toHaveAttribute("aria-hidden", "false");
|
||||
|
||||
const divExplorer = container.querySelector("#divExplorer");
|
||||
expect(divExplorer).toBeInTheDocument();
|
||||
expect(divExplorer).toHaveClass("flexContainer", "hideOverflows");
|
||||
|
||||
const freeTierBubble = container.querySelector("#freeTierTeachingBubble");
|
||||
expect(freeTierBubble).toBeInTheDocument();
|
||||
|
||||
const notificationContainer = container.querySelector("#explorerNotificationConsole");
|
||||
expect(notificationContainer).toBeInTheDocument();
|
||||
expect(notificationContainer).toHaveClass("dataExplorerErrorConsoleContainer");
|
||||
expect(notificationContainer).toHaveAttribute("role", "contentinfo");
|
||||
expect(notificationContainer).toHaveAttribute("aria-label", "Notification console");
|
||||
});
|
||||
|
||||
test("should apply correct inline styles", () => {
|
||||
const { container } = render(<ExplorerContainer explorer={mockExplorer} />);
|
||||
|
||||
const mainContainer = container.querySelector('[data-test="DataExplorerRoot"]');
|
||||
expect(mainContainer).toHaveStyle({
|
||||
flex: "1",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
});
|
||||
|
||||
const divExplorer = container.querySelector("#divExplorer");
|
||||
expect(divExplorer).toHaveStyle({
|
||||
flex: "1",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle carousel states correctly", () => {
|
||||
const mockUseCarousel = jest.mocked(useCarousel);
|
||||
|
||||
mockUseCarousel.mockImplementation((selector: { toString: () => string | string[] }) => {
|
||||
if (selector.toString().includes("shouldOpen")) {
|
||||
return false;
|
||||
}
|
||||
if (selector.toString().includes("showCopilotCarousel")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
render(<ExplorerContainer explorer={mockExplorer} />);
|
||||
|
||||
expect(screen.getByText("QuickstartCarousel - closed")).toBeInTheDocument();
|
||||
expect(screen.getByText("CopilotCarousel - open - with explorer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should call useInteractive hook with correct metric", () => {
|
||||
const mockUseInteractive = jest.mocked(useInteractive);
|
||||
|
||||
render(<ExplorerContainer explorer={mockExplorer} />);
|
||||
|
||||
expect(mockUseInteractive).toHaveBeenCalledWith("ApplicationLoad");
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from "react";
|
||||
import { Dialog } from "../Explorer/Controls/Dialog";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { CommandBar } from "../Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { NotificationConsole } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { SidePanel } from "../Explorer/Panes/PanelContainerComponent";
|
||||
import { QueryCopilotCarousel } from "../Explorer/QueryCopilot/CopilotCarousel";
|
||||
import { QuickstartCarousel } from "../Explorer/Quickstart/QuickstartCarousel";
|
||||
import { MongoQuickstartTutorial } from "../Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||
import { SQLQuickstartTutorial } from "../Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||
import { SidebarContainer } from "../Explorer/Sidebar";
|
||||
import { useCarousel } from "../hooks/useCarousel";
|
||||
import MetricScenario from "../Metrics/MetricEvents";
|
||||
import { useInteractive } from "../Metrics/useMetricPhases";
|
||||
|
||||
const ExplorerContainer: React.FC<{ explorer: Explorer }> = ({ explorer }) => {
|
||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||
useInteractive(MetricScenario.ApplicationLoad);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flexContainer"
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
data-test="DataExplorerRoot"
|
||||
>
|
||||
<div
|
||||
id="divExplorer"
|
||||
className="flexContainer hideOverflows"
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
}}
|
||||
>
|
||||
<div id="freeTierTeachingBubble"> </div>
|
||||
<CommandBar container={explorer} />
|
||||
<SidebarContainer explorer={explorer} />
|
||||
<div
|
||||
className="dataExplorerErrorConsoleContainer"
|
||||
role="contentinfo"
|
||||
aria-label="Notification console"
|
||||
id="explorerNotificationConsole"
|
||||
style={{
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
}}
|
||||
>
|
||||
<NotificationConsole />
|
||||
</div>
|
||||
</div>
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
{<SQLQuickstartTutorial />}
|
||||
{<MongoQuickstartTutorial />}
|
||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExplorerContainer;
|
||||
@@ -1,71 +0,0 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import LoadingExplorer from "./LoadingExplorer";
|
||||
|
||||
jest.mock("../../images/HdeConnectCosmosDB.svg", () => "test-hde-connect-image.svg");
|
||||
|
||||
jest.mock("@fluentui/react-components", () => ({
|
||||
makeStyles: jest.fn(() => () => ({
|
||||
root: "mock-root-class",
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("LoadingExplorer", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should render loading explorer component", () => {
|
||||
render(<LoadingExplorer />);
|
||||
|
||||
const container = screen.getByRole("alert");
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container).toHaveTextContent("Connecting...");
|
||||
});
|
||||
|
||||
test("should display welcome title", () => {
|
||||
render(<LoadingExplorer />);
|
||||
|
||||
const title = screen.getByText("Welcome to Azure Cosmos DB");
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveAttribute("id", "explorerLoadingStatusTitle");
|
||||
});
|
||||
|
||||
test("should display connecting status text", () => {
|
||||
render(<LoadingExplorer />);
|
||||
|
||||
const statusText = screen.getByText("Connecting...");
|
||||
expect(statusText).toBeInTheDocument();
|
||||
expect(statusText).toHaveAttribute("id", "explorerLoadingStatusText");
|
||||
expect(statusText).toHaveAttribute("role", "alert");
|
||||
});
|
||||
|
||||
test("should render Azure Cosmos DB image", () => {
|
||||
render(<LoadingExplorer />);
|
||||
|
||||
const image = screen.getByAltText("Azure Cosmos DB");
|
||||
expect(image).toBeInTheDocument();
|
||||
expect(image).toHaveAttribute("src", "test-hde-connect-image.svg");
|
||||
});
|
||||
|
||||
test("should have correct class structure", () => {
|
||||
render(<LoadingExplorer />);
|
||||
|
||||
const splashContainer = document.querySelector(".splashLoaderContainer");
|
||||
expect(splashContainer).toBeInTheDocument();
|
||||
|
||||
const contentContainer = document.querySelector(".splashLoaderContentContainer");
|
||||
expect(contentContainer).toBeInTheDocument();
|
||||
|
||||
const connectContent = document.querySelector(".connectExplorerContent");
|
||||
expect(connectContent).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should apply CSS classes correctly", () => {
|
||||
const { container } = render(<LoadingExplorer />);
|
||||
|
||||
const rootDiv = container.firstChild as HTMLElement;
|
||||
expect(rootDiv).toHaveClass("mock-root-class");
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { makeStyles } from "@fluentui/react-components";
|
||||
import React from "react";
|
||||
import hdeConnectImage from "../../images/HdeConnectCosmosDB.svg";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
height: "100vh",
|
||||
width: "100vw",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
},
|
||||
});
|
||||
|
||||
function LoadingExplorer(): JSX.Element {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="splashLoaderContainer">
|
||||
<div className="splashLoaderContentContainer">
|
||||
<p className="connectExplorerContent">
|
||||
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
||||
</p>
|
||||
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
||||
Welcome to Azure Cosmos DB
|
||||
</p>
|
||||
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
||||
Connecting...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingExplorer;
|
||||
@@ -1,107 +0,0 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import Root from "./Root";
|
||||
|
||||
jest.mock("../Explorer/ErrorBoundary", () => ({
|
||||
ErrorBoundary: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="mock-error-boundary">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@fluentui/react-components", () => ({
|
||||
FluentProvider: ({ children, theme }: { children: React.ReactNode; theme: { colorNeutralBackground1: string } }) => (
|
||||
<div
|
||||
data-testid="mock-fluent-provider"
|
||||
data-theme={theme.colorNeutralBackground1 === "dark" ? "webDarkTheme" : "webLightTheme"}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
webLightTheme: { colorNeutralBackground1: "light" },
|
||||
webDarkTheme: { colorNeutralBackground1: "dark" },
|
||||
}));
|
||||
|
||||
jest.mock("./App", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="mock-app">App</div>,
|
||||
}));
|
||||
|
||||
const createMockStore = (isDarkMode: boolean = false) => ({
|
||||
getState: jest.fn(() => ({ isDarkMode })),
|
||||
subscribe: jest.fn(() => jest.fn()),
|
||||
});
|
||||
|
||||
const mockThemeStore = createMockStore(false);
|
||||
|
||||
jest.mock("../hooks/useTheme", () => ({
|
||||
get useThemeStore() {
|
||||
return mockThemeStore;
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Root", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should render Root component with all child components", () => {
|
||||
render(<Root />);
|
||||
|
||||
expect(screen.getByTestId("mock-error-boundary")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-fluent-provider")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-app")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should have correct component hierarchy", () => {
|
||||
render(<Root />);
|
||||
|
||||
const errorBoundary = screen.getByTestId("mock-error-boundary");
|
||||
const fluentProvider = screen.getByTestId("mock-fluent-provider");
|
||||
const app = screen.getByTestId("mock-app");
|
||||
|
||||
expect(errorBoundary).toContainElement(fluentProvider);
|
||||
expect(fluentProvider).toContainElement(app);
|
||||
});
|
||||
|
||||
test("should subscribe to theme changes on mount", () => {
|
||||
render(<Root />);
|
||||
|
||||
expect(mockThemeStore.subscribe).toHaveBeenCalled();
|
||||
expect(mockThemeStore.subscribe).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
test("should get initial theme state", () => {
|
||||
render(<Root />);
|
||||
|
||||
expect(mockThemeStore.getState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle component unmounting", () => {
|
||||
const mockUnsubscribe = jest.fn();
|
||||
mockThemeStore.subscribe.mockReturnValue(mockUnsubscribe);
|
||||
|
||||
const { unmount } = render(<Root />);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockUnsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should call getState to initialize theme", () => {
|
||||
render(<Root />);
|
||||
|
||||
expect(mockThemeStore.getState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should handle theme subscription properly", () => {
|
||||
render(<Root />);
|
||||
|
||||
expect(mockThemeStore.subscribe).toHaveBeenCalledTimes(1);
|
||||
expect(mockThemeStore.getState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should render without errors", () => {
|
||||
expect(() => render(<Root />)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components";
|
||||
import React from "react";
|
||||
import { ErrorBoundary } from "../Explorer/ErrorBoundary";
|
||||
import { useThemeStore } from "../hooks/useTheme";
|
||||
import App from "./App";
|
||||
|
||||
const Root: React.FC = () => {
|
||||
// Use React state to track isDarkMode and subscribe to changes
|
||||
const [isDarkMode, setIsDarkMode] = React.useState(useThemeStore.getState().isDarkMode);
|
||||
const currentTheme = isDarkMode ? webDarkTheme : webLightTheme;
|
||||
|
||||
// Subscribe to theme changes
|
||||
React.useEffect(() => {
|
||||
return useThemeStore.subscribe((state) => {
|
||||
setIsDarkMode(state.isDarkMode);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<FluentProvider theme={currentTheme}>
|
||||
<App />
|
||||
</FluentProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default Root;
|
||||
@@ -316,11 +316,6 @@ body.isDarkMode {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// High specificity override for any nested elements
|
||||
* {
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
|
||||
// Ensure links maintain proper colors
|
||||
.ms-Link {
|
||||
color: var(--colorBrandForeground1);
|
||||
@@ -438,7 +433,6 @@ body.isDarkMode {
|
||||
|
||||
button {
|
||||
&:not(.ms-Button):not(.ms-IconButton) {
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
color: var(--colorNeutralForeground1);
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
--colorCompoundBrandStroke1: @SelectionColor;
|
||||
--colorBrandForeground1: @LinkColor;
|
||||
--colorPaletteRedForeground1: @ErrorColor;
|
||||
--colorSuccessGreen: #107c10;
|
||||
--overlayBackground: rgba(0, 0, 0, 0.4);
|
||||
--colorBrandBackground: @SelectionColor;
|
||||
--colorBrandBackgroundHover: @AccentMediumHigh;
|
||||
@@ -32,6 +33,7 @@ body.isDarkMode {
|
||||
--colorCompoundBrandStroke1: #4db6e8;
|
||||
--colorBrandForeground1: #4db6e8;
|
||||
--colorPaletteRedForeground1: #f25d5d;
|
||||
--colorSuccessGreen: #107c10;
|
||||
--overlayBackground: rgba(0, 0, 0, 0.4);
|
||||
--colorBrandBackground: #0078d4;
|
||||
--colorBrandBackgroundHover: #106ebe;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user