mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-23 19:54:08 +00:00
Compare commits
1 Commits
master
...
users/saks
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eade71456 |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -164,8 +164,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||||
shardTotal: [20]
|
shardTotal: [16]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
@@ -192,9 +192,6 @@ jobs:
|
|||||||
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||||
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
|
|
||||||
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
|
||||||
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||||
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
@@ -213,8 +210,6 @@ jobs:
|
|||||||
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||||
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
|
||||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
|
|
||||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
@@ -255,4 +250,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: html-report--attempt-${{ github.run_attempt }}
|
name: html-report--attempt-${{ github.run_attempt }}
|
||||||
path: playwright-report
|
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
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# Once every two hours
|
# Once every hour
|
||||||
- cron: "0 */2 * * *"
|
- cron: "0 15 * * *"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -36,4 +36,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: node utils/cleanupDBs.js
|
- run: node utils/cleanupDBs.js
|
||||||
|
|||||||
@@ -406,11 +406,7 @@ body {
|
|||||||
width: 440px;
|
width: 440px;
|
||||||
min-height: 565px;
|
min-height: 565px;
|
||||||
}
|
}
|
||||||
.dataExplorerLoaderforcopyJobs{
|
|
||||||
width: 100%;
|
|
||||||
min-height: 565px;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.dataExplorerTabLoaderContainer {
|
.dataExplorerTabLoaderContainer {
|
||||||
left: initial;
|
left: initial;
|
||||||
top: initial;
|
top: initial;
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ a:focus {
|
|||||||
|
|
||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-container {
|
.tabs-container {
|
||||||
|
|||||||
@@ -17,6 +17,38 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: -200px;
|
top: -200px;
|
||||||
}
|
}
|
||||||
|
body.isDarkMode .ms-Layer {
|
||||||
|
.ms-Callout-main {
|
||||||
|
background-color: @BaseHigh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Callout-beak {
|
||||||
|
background-color: @BaseHigh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-ContextualMenu {
|
||||||
|
background-color: @BaseHigh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Dropdown-items {
|
||||||
|
background-color: @BaseHigh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Dropdown-item {
|
||||||
|
background-color: @BaseHigh !important;
|
||||||
|
color: @BaseLight !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: @BaseMediumHigh !important;
|
||||||
|
color: @BaseLight !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
background-color: @BaseMediumHigh !important;
|
||||||
|
color: @BaseLight !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||||
@@ -129,6 +161,65 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.isDarkMode .accountSwitchContextualMenu {
|
||||||
|
background-color: @BaseHigh;
|
||||||
|
|
||||||
|
.ms-Callout-main {
|
||||||
|
background-color: @BaseHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-ContextualMenu-item {
|
||||||
|
background-color: @BaseHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Dropdown {
|
||||||
|
.ms-Dropdown-title {
|
||||||
|
background-color: @BaseDark;
|
||||||
|
color: @BaseLight;
|
||||||
|
border-color: @BaseMediumHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Dropdown-caretDownWrapper {
|
||||||
|
color: @BaseLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .ms-Dropdown-title {
|
||||||
|
background-color: @BaseHigh;
|
||||||
|
color: @BaseLight;
|
||||||
|
border-color: @BaseMedium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Label {
|
||||||
|
color: @BaseLight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isDarkMode .ms-Dropdown-callout {
|
||||||
|
.ms-Callout-main {
|
||||||
|
background-color: @BaseHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Dropdown-items {
|
||||||
|
background-color: @BaseHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Dropdown-item {
|
||||||
|
background-color: @BaseHigh;
|
||||||
|
color: @BaseLight;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: @BaseMediumHigh;
|
||||||
|
color: @BaseLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
background-color: @BaseMediumHigh;
|
||||||
|
color: @BaseLight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.fixedleftpane {
|
.fixedleftpane {
|
||||||
background: @BaseLow;
|
background: @BaseLow;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -116,8 +116,8 @@
|
|||||||
"tinykeys": "2.1.0",
|
"tinykeys": "2.1.0",
|
||||||
"underscore": "1.12.1",
|
"underscore": "1.12.1",
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.10.0",
|
||||||
"uuid": "9.0.0",
|
|
||||||
"web-vitals": "4.2.4",
|
"web-vitals": "4.2.4",
|
||||||
|
"uuid": "9.0.0",
|
||||||
"zustand": "3.5.0"
|
"zustand": "3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export default defineConfig({
|
|||||||
reporter: process.env.CI ? "blob" : "html",
|
reporter: process.env.CI ? "blob" : "html",
|
||||||
timeout: 10 * 60 * 1000,
|
timeout: 10 * 60 * 1000,
|
||||||
use: {
|
use: {
|
||||||
trace: "retain-on-failure",
|
trace: "off",
|
||||||
video: "retain-on-failure",
|
video: "off",
|
||||||
screenshot: "on",
|
screenshot: "on",
|
||||||
testIdAttribute: "data-test",
|
testIdAttribute: "data-test",
|
||||||
contextOptions: {
|
contextOptions: {
|
||||||
|
|||||||
@@ -7,27 +7,16 @@ import { HttpStatusCodes } from "./Constants";
|
|||||||
import { logError } from "./Logger";
|
import { logError } from "./Logger";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
export interface HandleErrorOptions {
|
export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => {
|
||||||
/** Optional redacted error to use for telemetry logging instead of the original error */
|
|
||||||
redactedError?: string | ARMError | Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handleError = (
|
|
||||||
error: string | ARMError | Error,
|
|
||||||
area: string,
|
|
||||||
consoleErrorPrefix?: string,
|
|
||||||
options?: HandleErrorOptions,
|
|
||||||
): void => {
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
const errorCode = error instanceof ARMError ? error.code : undefined;
|
const errorCode = error instanceof ARMError ? error.code : undefined;
|
||||||
|
|
||||||
// logs error to data explorer console (always shows original, non-redacted message)
|
// logs error to data explorer console
|
||||||
const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage;
|
const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage;
|
||||||
logConsoleError(consoleErrorMessage);
|
logConsoleError(consoleErrorMessage);
|
||||||
|
|
||||||
// logs error to both app insight and kusto (use redacted message if provided)
|
// logs error to both app insight and kusto
|
||||||
const telemetryErrorMessage = options?.redactedError ? getErrorMessage(options.redactedError) : errorMessage;
|
logError(errorMessage, area, errorCode);
|
||||||
logError(telemetryErrorMessage, area, errorCode);
|
|
||||||
|
|
||||||
// checks for errors caused by firewall and sends them to portal to handle
|
// checks for errors caused by firewall and sends them to portal to handle
|
||||||
sendNotificationForError(errorMessage, errorCode);
|
sendNotificationForError(errorMessage, errorCode);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
|
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
|
||||||
import { useThemeStore } from "hooks/useTheme";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface LoadingOverlayProps {
|
interface LoadingOverlayProps {
|
||||||
@@ -8,17 +7,15 @@ interface LoadingOverlayProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
|
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
|
||||||
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
data-test="loading-overlay"
|
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: isDarkMode ? "rgba(32, 31, 30, 0.9)" : "rgba(255,255,255,0.9)",
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -26,11 +23,7 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Spinner
|
<Spinner size={SpinnerSize.large} label={label} styles={{ label: { fontWeight: 600 } }} />
|
||||||
size={SpinnerSize.large}
|
|
||||||
label={label}
|
|
||||||
styles={{ label: { fontWeight: 600, color: isDarkMode ? "#ffffff" : "#323130" } }}
|
|
||||||
/>
|
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,14 +11,3 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override dark mode inherit for pagination icons */
|
|
||||||
body.isDarkMode .pager-container .ms-Button .ms-Button-icon,
|
|
||||||
body.isDarkMode .pager-container .ms-Button i {
|
|
||||||
color: var(--colorBrandForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.isDarkMode .pager-container .ms-Button:disabled .ms-Button-icon,
|
|
||||||
body.isDarkMode .pager-container .ms-Button:disabled i {
|
|
||||||
color: var(--colorNeutralForegroundDisabled);
|
|
||||||
}
|
|
||||||
@@ -59,7 +59,7 @@ const Pager: React.FC<PagerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={className || "pager-container"}>
|
<div className={className || "pager-container"}>
|
||||||
{showItemCount && (
|
{showItemCount && (
|
||||||
<Text className="themeText">
|
<Text>
|
||||||
Showing {startIndex + 1} - {endIndex} of {totalCount} items
|
Showing {startIndex + 1} - {endIndex} of {totalCount} items
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -82,7 +82,7 @@ const Pager: React.FC<PagerProps> = ({
|
|||||||
disabled={disabled || currentPage === 1}
|
disabled={disabled || currentPage === 1}
|
||||||
styles={iconButtonStyles}
|
styles={iconButtonStyles}
|
||||||
/>
|
/>
|
||||||
<Text className="themeText">
|
<Text>
|
||||||
Page {currentPage} of {totalPages}
|
Page {currentPage} of {totalPages}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
exports[`LoadingOverlay should handle long labels properly 1`] = `
|
exports[`LoadingOverlay should handle long labels properly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
data-test="loading-overlay"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
@@ -23,7 +22,6 @@ exports[`LoadingOverlay should handle long labels properly 1`] = `
|
|||||||
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
|
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
data-test="loading-overlay"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
@@ -43,7 +41,6 @@ exports[`LoadingOverlay should render loading overlay when isLoading is true 1`]
|
|||||||
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
|
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
data-test="loading-overlay"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
@@ -63,7 +60,6 @@ exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
|
|||||||
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
|
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-109"
|
class="ms-Overlay root-109"
|
||||||
data-test="loading-overlay"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-111"
|
class="ms-Spinner root-111"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
SqlStoredProcedureCreateUpdateParameters,
|
SqlStoredProcedureCreateUpdateParameters,
|
||||||
SqlStoredProcedureResource,
|
SqlStoredProcedureResource,
|
||||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ export async function createStoredProcedure(
|
|||||||
): Promise<StoredProcedureDefinition & Resource> {
|
): Promise<StoredProcedureDefinition & Resource> {
|
||||||
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
|
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
|
||||||
try {
|
try {
|
||||||
let resource: StoredProcedureDefinition & Resource;
|
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
@@ -61,16 +60,14 @@ export async function createStoredProcedure(
|
|||||||
storedProcedure.id,
|
storedProcedure.id,
|
||||||
createSprocParams,
|
createSprocParams,
|
||||||
);
|
);
|
||||||
resource = rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
||||||
} else {
|
|
||||||
const response = await client()
|
|
||||||
.database(databaseId)
|
|
||||||
.container(collectionId)
|
|
||||||
.scripts.storedProcedures.create(storedProcedure);
|
|
||||||
resource = response.resource;
|
|
||||||
}
|
}
|
||||||
logConsoleInfo(`Successfully created stored procedure ${storedProcedure.id}`);
|
|
||||||
return resource;
|
const response = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.container(collectionId)
|
||||||
|
.scripts.storedProcedures.create(storedProcedure);
|
||||||
|
return response?.resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`);
|
handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AuthType } from "../../AuthType";
|
|||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ export async function createTrigger(
|
|||||||
): Promise<TriggerDefinition | SqlTriggerResource> {
|
): Promise<TriggerDefinition | SqlTriggerResource> {
|
||||||
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
|
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
|
||||||
try {
|
try {
|
||||||
let resource: SqlTriggerResource | TriggerDefinition;
|
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
@@ -53,16 +52,14 @@ export async function createTrigger(
|
|||||||
trigger.id,
|
trigger.id,
|
||||||
createTriggerParams,
|
createTriggerParams,
|
||||||
);
|
);
|
||||||
resource = rpResponse && rpResponse.properties?.resource;
|
return 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;
|
|
||||||
}
|
}
|
||||||
logConsoleInfo(`Successfully created trigger ${trigger.id}`);
|
|
||||||
return 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;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`);
|
handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
SqlUserDefinedFunctionCreateUpdateParameters,
|
SqlUserDefinedFunctionCreateUpdateParameters,
|
||||||
SqlUserDefinedFunctionResource,
|
SqlUserDefinedFunctionResource,
|
||||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ export async function createUserDefinedFunction(
|
|||||||
): Promise<UserDefinedFunctionDefinition & Resource> {
|
): Promise<UserDefinedFunctionDefinition & Resource> {
|
||||||
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
|
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
|
||||||
try {
|
try {
|
||||||
let resource: UserDefinedFunctionDefinition & Resource;
|
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
@@ -61,17 +60,14 @@ export async function createUserDefinedFunction(
|
|||||||
userDefinedFunction.id,
|
userDefinedFunction.id,
|
||||||
createUDFParams,
|
createUDFParams,
|
||||||
);
|
);
|
||||||
|
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||||
resource = rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
|
||||||
} else {
|
|
||||||
const response = await client()
|
|
||||||
.database(databaseId)
|
|
||||||
.container(collectionId)
|
|
||||||
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
|
||||||
resource = response.resource;
|
|
||||||
}
|
}
|
||||||
logConsoleInfo(`Successfully created user defined function ${userDefinedFunction.id}`);
|
|
||||||
return resource;
|
const response = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.container(collectionId)
|
||||||
|
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
||||||
|
return response?.resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { configContext } from "ConfigContext";
|
|
||||||
import { ApiType, userContext } from "UserContext";
|
import { ApiType, userContext } from "UserContext";
|
||||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||||
import {
|
import {
|
||||||
@@ -15,12 +14,9 @@ import {
|
|||||||
DataTransferJobFeedResults,
|
DataTransferJobFeedResults,
|
||||||
DataTransferJobGetResults,
|
DataTransferJobGetResults,
|
||||||
} from "Utils/arm/generatedClients/dataTransferService/types";
|
} from "Utils/arm/generatedClients/dataTransferService/types";
|
||||||
import { armRequest } from "Utils/arm/request";
|
|
||||||
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
||||||
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
||||||
|
|
||||||
export const DATA_TRANSFER_JOB_API_VERSION = "2025-05-01-preview";
|
|
||||||
|
|
||||||
export interface DataTransferParams {
|
export interface DataTransferParams {
|
||||||
jobName: string;
|
jobName: string;
|
||||||
apiType: ApiType;
|
apiType: ApiType;
|
||||||
@@ -37,34 +33,26 @@ export const getDataTransferJobs = async (
|
|||||||
subscriptionId: string,
|
subscriptionId: string,
|
||||||
resourceGroup: string,
|
resourceGroup: string,
|
||||||
accountName: string,
|
accountName: string,
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<DataTransferJobGetResults[]> => {
|
): Promise<DataTransferJobGetResults[]> => {
|
||||||
let dataTransferJobs: DataTransferJobGetResults[] = [];
|
let dataTransferJobs: DataTransferJobGetResults[] = [];
|
||||||
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
|
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
resourceGroup,
|
resourceGroup,
|
||||||
accountName,
|
accountName,
|
||||||
signal,
|
|
||||||
);
|
);
|
||||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
||||||
while (dataTransferFeeds?.nextLink) {
|
while (dataTransferFeeds?.nextLink) {
|
||||||
/**
|
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
|
||||||
* The `nextLink` URL returned by the Cosmos DB SQL API pointed to an incorrect endpoint, causing timeouts.
|
headers: {
|
||||||
* (i.e: https://cdbmgmtprodby.documents.azure.com:450/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.DocumentDB/databaseAccounts/{account}/sql/dataTransferJobs?$top=100&$skiptoken=...)
|
Authorization: userContext.authorizationToken,
|
||||||
* 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,
|
|
||||||
});
|
});
|
||||||
dataTransferJobs.push(...(dataTransferFeeds?.value || []));
|
if (nextResponse.ok) {
|
||||||
|
dataTransferFeeds = await nextResponse.json();
|
||||||
|
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return dataTransferJobs;
|
return dataTransferJobs;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ export const deleteDocuments = async (
|
|||||||
documentIds: DocumentId[],
|
documentIds: DocumentId[],
|
||||||
abortSignal: AbortSignal,
|
abortSignal: AbortSignal,
|
||||||
): Promise<IBulkDeleteResult[]> => {
|
): Promise<IBulkDeleteResult[]> => {
|
||||||
const totalCount = documentIds.length;
|
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||||
const clearMessage = logConsoleProgress(`Deleting ${totalCount} ${getEntityName(true)}`);
|
|
||||||
try {
|
try {
|
||||||
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
||||||
|
|
||||||
@@ -84,7 +83,11 @@ export const deleteDocuments = async (
|
|||||||
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
||||||
return flatAllResult;
|
return flatAllResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteDocuments", `Error while deleting ${totalCount} ${getEntityName(totalCount > 1)}`);
|
handleError(
|
||||||
|
error,
|
||||||
|
"DeleteDocuments",
|
||||||
|
`Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`,
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export async function deleteStoredProcedure(
|
|||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
|
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
|
||||||
}
|
}
|
||||||
logConsoleProgress(`Successfully deleted stored procedure ${storedProcedureId}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
|
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
|
|||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
|
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
|
||||||
}
|
}
|
||||||
logConsoleProgress(`Successfully deleted trigger ${triggerId}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
|
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
|
|||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
|
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
|
||||||
}
|
}
|
||||||
logConsoleProgress(`Successfully deleted user defined function ${id}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
|
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
import { redactSyntaxErrorMessage } from "./queryDocumentsPage";
|
|
||||||
|
|
||||||
/* Typical error to redact looks like this (the message property contains a JSON string with nested structure):
|
|
||||||
{
|
|
||||||
"message": "{\"code\":\"BadRequest\",\"message\":\"{\\\"errors\\\":[{\\\"severity\\\":\\\"Error\\\",\\\"location\\\":{\\\"start\\\":0,\\\"end\\\":5},\\\"code\\\":\\\"SC1001\\\",\\\"message\\\":\\\"Syntax error, incorrect syntax near 'Crazy'.\\\"}]}\\r\\nActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0\"}"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Helper to create the nested error structure that matches what the SDK returns
|
|
||||||
const createNestedError = (
|
|
||||||
errors: Array<{ severity?: string; location?: { start: number; end: number }; code: string; message: string }>,
|
|
||||||
activityId: string = "test-activity-id",
|
|
||||||
): { message: string } => {
|
|
||||||
const innerErrorsJson = JSON.stringify({ errors });
|
|
||||||
const innerMessage = `${innerErrorsJson}\r\n${activityId}`;
|
|
||||||
const outerJson = JSON.stringify({ code: "BadRequest", message: innerMessage });
|
|
||||||
return { message: outerJson };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper to parse the redacted result
|
|
||||||
const parseRedactedResult = (result: { message: string }) => {
|
|
||||||
const outerParsed = JSON.parse(result.message);
|
|
||||||
const [innerErrorsJson, activityIdPart] = outerParsed.message.split("\r\n");
|
|
||||||
const innerErrors = JSON.parse(innerErrorsJson);
|
|
||||||
return { outerParsed, innerErrors, activityIdPart };
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("redactSyntaxErrorMessage", () => {
|
|
||||||
it("should redact SC1001 error message", () => {
|
|
||||||
const error = createNestedError(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
severity: "Error",
|
|
||||||
location: { start: 0, end: 5 },
|
|
||||||
code: "SC1001",
|
|
||||||
message: "Syntax error, incorrect syntax near 'Crazy'.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17",
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error) as { message: string };
|
|
||||||
const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result);
|
|
||||||
|
|
||||||
expect(outerParsed.code).toBe("BadRequest");
|
|
||||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
|
||||||
expect(activityIdPart).toContain("ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should redact SC2001 error message", () => {
|
|
||||||
const error = createNestedError(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
severity: "Error",
|
|
||||||
location: { start: 0, end: 10 },
|
|
||||||
code: "SC2001",
|
|
||||||
message: "Some sensitive syntax error message.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"ActivityId: abc123",
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error) as { message: string };
|
|
||||||
const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result);
|
|
||||||
|
|
||||||
expect(outerParsed.code).toBe("BadRequest");
|
|
||||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
|
||||||
expect(activityIdPart).toContain("ActivityId: abc123");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should redact multiple errors with SC1001 and SC2001 codes", () => {
|
|
||||||
const error = createNestedError(
|
|
||||||
[
|
|
||||||
{ severity: "Error", code: "SC1001", message: "First error" },
|
|
||||||
{ severity: "Error", code: "SC2001", message: "Second error" },
|
|
||||||
],
|
|
||||||
"ActivityId: xyz",
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error) as { message: string };
|
|
||||||
const { innerErrors } = parseRedactedResult(result);
|
|
||||||
|
|
||||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
|
||||||
expect(innerErrors.errors[1].message).toBe("__REDACTED__");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not redact errors with other codes", () => {
|
|
||||||
const error = createNestedError(
|
|
||||||
[{ severity: "Error", code: "SC9999", message: "This should not be redacted." }],
|
|
||||||
"ActivityId: test123",
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error);
|
|
||||||
|
|
||||||
expect(result).toBe(error); // Should return original error unchanged
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not modify non-BadRequest errors", () => {
|
|
||||||
const innerMessage = JSON.stringify({ errors: [{ code: "SC1001", message: "Should not be redacted" }] });
|
|
||||||
const error = {
|
|
||||||
message: JSON.stringify({ code: "NotFound", message: innerMessage }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error);
|
|
||||||
|
|
||||||
expect(result).toBe(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle errors without message property", () => {
|
|
||||||
const error = { code: "BadRequest" };
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error);
|
|
||||||
|
|
||||||
expect(result).toBe(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle non-object errors", () => {
|
|
||||||
const stringError = "Simple string error";
|
|
||||||
const nullError: null = null;
|
|
||||||
const undefinedError: undefined = undefined;
|
|
||||||
|
|
||||||
expect(redactSyntaxErrorMessage(stringError)).toBe(stringError);
|
|
||||||
expect(redactSyntaxErrorMessage(nullError)).toBe(nullError);
|
|
||||||
expect(redactSyntaxErrorMessage(undefinedError)).toBe(undefinedError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle malformed JSON in message", () => {
|
|
||||||
const error = {
|
|
||||||
message: "not valid json",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error);
|
|
||||||
|
|
||||||
expect(result).toBe(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle message without ActivityId suffix", () => {
|
|
||||||
const innerErrorsJson = JSON.stringify({
|
|
||||||
errors: [{ severity: "Error", code: "SC1001", message: "Syntax error near something." }],
|
|
||||||
});
|
|
||||||
const error = {
|
|
||||||
message: JSON.stringify({ code: "BadRequest", message: innerErrorsJson + "\r\n" }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error) as { message: string };
|
|
||||||
const { innerErrors } = parseRedactedResult(result);
|
|
||||||
|
|
||||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve other error properties", () => {
|
|
||||||
const baseError = createNestedError([{ code: "SC1001", message: "Error" }], "ActivityId: test");
|
|
||||||
const error = {
|
|
||||||
...baseError,
|
|
||||||
statusCode: 400,
|
|
||||||
additionalInfo: "extra data",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = redactSyntaxErrorMessage(error) as {
|
|
||||||
message: string;
|
|
||||||
statusCode: number;
|
|
||||||
additionalInfo: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(result.statusCode).toBe(400);
|
|
||||||
expect(result.additionalInfo).toBe("extra data");
|
|
||||||
|
|
||||||
const { innerErrors } = parseRedactedResult(result);
|
|
||||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,51 +4,6 @@ import { getEntityName } from "../DocumentUtility";
|
|||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
|
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
|
||||||
|
|
||||||
// Redact sensitive information from BadRequest errors with specific codes
|
|
||||||
export const redactSyntaxErrorMessage = (error: unknown): unknown => {
|
|
||||||
const codesToRedact = ["SC1001", "SC2001"];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Handle error objects with a message property
|
|
||||||
if (error && typeof error === "object" && "message" in error) {
|
|
||||||
const errorObj = error as { code?: string; message?: string };
|
|
||||||
if (typeof errorObj.message === "string") {
|
|
||||||
// Parse the inner JSON from the message
|
|
||||||
const innerJson = JSON.parse(errorObj.message);
|
|
||||||
if (innerJson.code === "BadRequest" && typeof innerJson.message === "string") {
|
|
||||||
const [innerErrorsJson, activityIdPart] = innerJson.message.split("\r\n");
|
|
||||||
const innerErrorsObj = JSON.parse(innerErrorsJson);
|
|
||||||
if (Array.isArray(innerErrorsObj.errors)) {
|
|
||||||
let modified = false;
|
|
||||||
innerErrorsObj.errors = innerErrorsObj.errors.map((err: { code?: string; message?: string }) => {
|
|
||||||
if (err.code && codesToRedact.includes(err.code)) {
|
|
||||||
modified = true;
|
|
||||||
return { ...err, message: "__REDACTED__" };
|
|
||||||
}
|
|
||||||
return err;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (modified) {
|
|
||||||
// Reconstruct the message with the redacted content
|
|
||||||
const redactedMessage = JSON.stringify(innerErrorsObj) + `\r\n${activityIdPart}`;
|
|
||||||
const redactedError = {
|
|
||||||
...error,
|
|
||||||
message: JSON.stringify({ ...innerJson, message: redactedMessage }),
|
|
||||||
body: undefined as unknown, // Clear body to avoid sensitive data
|
|
||||||
};
|
|
||||||
return redactedError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// If parsing fails, return the original error
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const queryDocumentsPage = async (
|
export const queryDocumentsPage = async (
|
||||||
resourceName: string,
|
resourceName: string,
|
||||||
documentsIterator: MinimalQueryIterator,
|
documentsIterator: MinimalQueryIterator,
|
||||||
@@ -63,12 +18,7 @@ export const queryDocumentsPage = async (
|
|||||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Redact sensitive information for telemetry while showing original in console
|
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
|
||||||
const redactedError = redactSyntaxErrorMessage(error);
|
|
||||||
|
|
||||||
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`, {
|
|
||||||
redactedError: redactedError as Error,
|
|
||||||
});
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearMessage();
|
clearMessage();
|
||||||
|
|||||||
@@ -46,10 +46,6 @@ export type DataExploreMessageV3 =
|
|||||||
params: {
|
params: {
|
||||||
updateType: "created" | "deleted" | "settings";
|
updateType: "created" | "deleted" | "settings";
|
||||||
};
|
};
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: FabricMessageTypes.RestoreContainer;
|
|
||||||
params: [];
|
|
||||||
};
|
};
|
||||||
export interface GetCosmosTokenMessageOptions {
|
export interface GetCosmosTokenMessageOptions {
|
||||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
|
||||||
import * as Logger from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||||
@@ -31,7 +30,6 @@ jest.mock("../../../Common/Logger");
|
|||||||
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
|
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
|
||||||
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
||||||
jest.mock("../CopyJobUtils");
|
jest.mock("../CopyJobUtils");
|
||||||
jest.mock("../../../Common/dataAccess/dataTransfers");
|
|
||||||
|
|
||||||
describe("CopyJobActions", () => {
|
describe("CopyJobActions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -156,31 +154,33 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should fetch and format copy jobs successfully", async () => {
|
it("should fetch and format copy jobs successfully", async () => {
|
||||||
const mockResponse = [
|
const mockResponse = {
|
||||||
{
|
value: [
|
||||||
properties: {
|
{
|
||||||
jobName: "job-1",
|
properties: {
|
||||||
status: "InProgress",
|
jobName: "job-1",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
status: "InProgress",
|
||||||
processedCount: 50,
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 50,
|
||||||
mode: "online",
|
totalCount: 100,
|
||||||
duration: "01:30:45",
|
mode: "online",
|
||||||
source: {
|
duration: "01:30:45",
|
||||||
component: "CosmosDBSql",
|
source: {
|
||||||
databaseName: "source-db",
|
component: "CosmosDBSql",
|
||||||
containerName: "source-container",
|
databaseName: "source-db",
|
||||||
},
|
containerName: "source-container",
|
||||||
destination: {
|
},
|
||||||
component: "CosmosDBSql",
|
destination: {
|
||||||
databaseName: "target-db",
|
component: "CosmosDBSql",
|
||||||
containerName: "target-container",
|
databaseName: "target-db",
|
||||||
|
containerName: "target-container",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
];
|
};
|
||||||
|
|
||||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -201,36 +201,38 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should filter jobs by CosmosDBSql component", async () => {
|
it("should filter jobs by CosmosDBSql component", async () => {
|
||||||
const mockResponse = [
|
const mockResponse = {
|
||||||
{
|
value: [
|
||||||
properties: {
|
{
|
||||||
jobName: "sql-job",
|
properties: {
|
||||||
status: "Completed",
|
jobName: "sql-job",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
status: "Completed",
|
||||||
processedCount: 100,
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 100,
|
||||||
mode: "offline",
|
totalCount: 100,
|
||||||
duration: "02:00:00",
|
mode: "offline",
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
duration: "02:00:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "other-job",
|
||||||
jobName: "other-job",
|
status: "Completed",
|
||||||
status: "Completed",
|
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
processedCount: 100,
|
||||||
processedCount: 100,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "offline",
|
||||||
mode: "offline",
|
duration: "01:00:00",
|
||||||
duration: "01:00:00",
|
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
];
|
};
|
||||||
|
|
||||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -245,36 +247,38 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should sort jobs by last updated time (newest first)", async () => {
|
it("should sort jobs by last updated time (newest first)", async () => {
|
||||||
const mockResponse = [
|
const mockResponse = {
|
||||||
{
|
value: [
|
||||||
properties: {
|
{
|
||||||
jobName: "older-job",
|
properties: {
|
||||||
status: "Completed",
|
jobName: "older-job",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
status: "Completed",
|
||||||
processedCount: 100,
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 100,
|
||||||
mode: "offline",
|
totalCount: 100,
|
||||||
duration: "01:00:00",
|
mode: "offline",
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
duration: "01:00:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "newer-job",
|
||||||
jobName: "newer-job",
|
status: "InProgress",
|
||||||
status: "InProgress",
|
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
processedCount: 50,
|
||||||
processedCount: 50,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "online",
|
||||||
mode: "online",
|
duration: "00:30:00",
|
||||||
duration: "00:30:00",
|
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
];
|
};
|
||||||
|
|
||||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -289,23 +293,25 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should calculate completion percentage correctly", async () => {
|
it("should calculate completion percentage correctly", async () => {
|
||||||
const mockResponse = [
|
const mockResponse = {
|
||||||
{
|
value: [
|
||||||
properties: {
|
{
|
||||||
jobName: "job-1",
|
properties: {
|
||||||
status: "InProgress",
|
jobName: "job-1",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
status: "InProgress",
|
||||||
processedCount: 75,
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 75,
|
||||||
mode: "online",
|
totalCount: 100,
|
||||||
duration: "01:00:00",
|
mode: "online",
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
duration: "01:00:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
];
|
};
|
||||||
|
|
||||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -319,23 +325,25 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle zero total count gracefully", async () => {
|
it("should handle zero total count gracefully", async () => {
|
||||||
const mockResponse = [
|
const mockResponse = {
|
||||||
{
|
value: [
|
||||||
properties: {
|
{
|
||||||
jobName: "job-1",
|
properties: {
|
||||||
status: "Pending",
|
jobName: "job-1",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
status: "Pending",
|
||||||
processedCount: 0,
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
totalCount: 0,
|
processedCount: 0,
|
||||||
mode: "online",
|
totalCount: 0,
|
||||||
duration: "00:00:00",
|
mode: "online",
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
duration: "00:00:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
];
|
};
|
||||||
|
|
||||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -353,24 +361,26 @@ describe("CopyJobActions", () => {
|
|||||||
message: "Error message line 1\r\n\r\nError message line 2",
|
message: "Error message line 1\r\n\r\nError message line 2",
|
||||||
code: "ErrorCode123",
|
code: "ErrorCode123",
|
||||||
};
|
};
|
||||||
const mockResponse = [
|
const mockResponse = {
|
||||||
{
|
value: [
|
||||||
properties: {
|
{
|
||||||
jobName: "failed-job",
|
properties: {
|
||||||
status: "Failed",
|
jobName: "failed-job",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
status: "Failed",
|
||||||
processedCount: 50,
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 50,
|
||||||
mode: "offline",
|
totalCount: 100,
|
||||||
duration: "00:30:00",
|
mode: "offline",
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
duration: "00:30:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
error: mockError,
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
|
error: mockError,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
];
|
};
|
||||||
|
|
||||||
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -398,7 +408,7 @@ describe("CopyJobActions", () => {
|
|||||||
};
|
};
|
||||||
(global as any).AbortController = jest.fn(() => mockAbortController);
|
(global as any).AbortController = jest.fn(() => mockAbortController);
|
||||||
|
|
||||||
(getDataTransferJobs as jest.Mock).mockResolvedValue([]);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ value: [] });
|
||||||
|
|
||||||
getCopyJobs();
|
getCopyJobs();
|
||||||
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
||||||
@@ -408,7 +418,9 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error for invalid response format", async () => {
|
it("should throw error for invalid response format", async () => {
|
||||||
(getDataTransferJobs as jest.Mock).mockResolvedValue("not-an-array");
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({
|
||||||
|
value: "not-an-array",
|
||||||
|
});
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
|
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
|
||||||
});
|
});
|
||||||
@@ -418,7 +430,7 @@ describe("CopyJobActions", () => {
|
|||||||
message: "Aborted",
|
message: "Aborted",
|
||||||
content: JSON.stringify({ message: "signal is aborted without reason" }),
|
content: JSON.stringify({ message: "signal is aborted without reason" }),
|
||||||
};
|
};
|
||||||
(getDataTransferJobs as jest.Mock).mockRejectedValue(abortError);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError);
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toMatchObject({
|
await expect(getCopyJobs()).rejects.toMatchObject({
|
||||||
message: expect.stringContaining("Previous copy job request was cancelled."),
|
message: expect.stringContaining("Previous copy job request was cancelled."),
|
||||||
@@ -427,7 +439,7 @@ describe("CopyJobActions", () => {
|
|||||||
|
|
||||||
it("should handle generic errors", async () => {
|
it("should handle generic errors", async () => {
|
||||||
const genericError = new Error("Network error");
|
const genericError = new Error("Network error");
|
||||||
(getDataTransferJobs as jest.Mock).mockRejectedValue(genericError);
|
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(genericError);
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toThrow("Network error");
|
await expect(getCopyJobs()).rejects.toThrow("Network error");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
|
||||||
import { logError } from "../../../Common/Logger";
|
import { logError } from "../../../Common/Logger";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import {
|
import {
|
||||||
cancel,
|
cancel,
|
||||||
complete,
|
complete,
|
||||||
create,
|
create,
|
||||||
|
listByDatabaseAccount,
|
||||||
pause,
|
pause,
|
||||||
resume,
|
resume,
|
||||||
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||||
@@ -63,8 +63,14 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
|||||||
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
||||||
userContext.databaseAccount?.id || "",
|
userContext.databaseAccount?.id || "",
|
||||||
);
|
);
|
||||||
const jobs = await getDataTransferJobs(subscriptionId, resourceGroup, accountName, copyJobsAbortController.signal);
|
const response = await listByDatabaseAccount(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
copyJobsAbortController.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
const jobs = response.value || [];
|
||||||
if (!Array.isArray(jobs)) {
|
if (!Array.isArray(jobs)) {
|
||||||
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe("CopyJobCommandBar", () => {
|
|||||||
|
|
||||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ describe("CopyJobCommandBar", () => {
|
|||||||
|
|
||||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
||||||
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
|
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,11 +175,11 @@ describe("CopyJobCommandBar", () => {
|
|||||||
mockConvertButton.mockReturnValue([]);
|
mockConvertButton.mockReturnValue([]);
|
||||||
|
|
||||||
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
|
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1, false);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1);
|
||||||
|
|
||||||
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
|
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2, false);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useThemeStore } from "../../../hooks/useTheme";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
|
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
|
||||||
import { getThemeTokens } from "../../Theme/ThemeUtil";
|
|
||||||
import { ContainerCopyProps } from "../Types/CopyJobTypes";
|
import { ContainerCopyProps } from "../Types/CopyJobTypes";
|
||||||
import { getCommandBarButtons } from "./Utils";
|
import { getCommandBarButtons } from "./Utils";
|
||||||
|
|
||||||
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
|
const rootStyle = {
|
||||||
|
root: {
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
||||||
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer);
|
||||||
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);
|
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer" style={{ backgroundColor }}>
|
<div className="commandBarContainer">
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
styles={rootStyle}
|
styles={rootStyle}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe("CommandBar Utils", () => {
|
|||||||
|
|
||||||
describe("getCommandBarButtons", () => {
|
describe("getCommandBarButtons", () => {
|
||||||
it("should return an array of command button props", () => {
|
it("should return an array of command button props", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
expect(buttons).toBeDefined();
|
expect(buttons).toBeDefined();
|
||||||
expect(Array.isArray(buttons)).toBe(true);
|
expect(Array.isArray(buttons)).toBe(true);
|
||||||
@@ -58,7 +58,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include create copy job button", () => {
|
it("should include create copy job button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
const createButton = buttons[0];
|
const createButton = buttons[0];
|
||||||
|
|
||||||
expect(createButton).toBeDefined();
|
expect(createButton).toBeDefined();
|
||||||
@@ -70,7 +70,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include refresh button", () => {
|
it("should include refresh button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
expect(refreshButton).toBeDefined();
|
expect(refreshButton).toBeDefined();
|
||||||
@@ -80,11 +80,11 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include feedback button when platform is Portal", () => {
|
it("should include feedback button when platform is Portal", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
expect(buttons.length).toBe(4);
|
expect(buttons.length).toBe(3);
|
||||||
|
|
||||||
const feedbackButton = buttons[3];
|
const feedbackButton = buttons[2];
|
||||||
expect(feedbackButton).toBeDefined();
|
expect(feedbackButton).toBeDefined();
|
||||||
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
|
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
|
||||||
expect(feedbackButton.tooltipText).toBe("Feedback");
|
expect(feedbackButton.tooltipText).toBe("Feedback");
|
||||||
@@ -105,13 +105,13 @@ describe("CommandBar Utils", () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
|
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
|
||||||
const buttons = getCommandBarButtonsEmulator(mockExplorer, false);
|
const buttons = getCommandBarButtonsEmulator(mockExplorer);
|
||||||
|
|
||||||
expect(buttons.length).toBe(3);
|
expect(buttons.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call openCreateCopyJobPanel when create button is clicked", () => {
|
it("should call openCreateCopyJobPanel when create button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
const createButton = buttons[0];
|
const createButton = buttons[0];
|
||||||
|
|
||||||
createButton.onCommandClick({} as React.SyntheticEvent);
|
createButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
@@ -121,7 +121,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call refreshJobList when refresh button is clicked", () => {
|
it("should call refreshJobList when refresh button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
refreshButton.onCommandClick({} as React.SyntheticEvent);
|
refreshButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
@@ -130,8 +130,8 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
|
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
const feedbackButton = buttons[3];
|
const feedbackButton = buttons[2];
|
||||||
|
|
||||||
feedbackButton.onCommandClick({} as React.SyntheticEvent);
|
feedbackButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return buttons with correct icon sources", () => {
|
it("should return buttons with correct icon sources", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
expect(buttons[0].iconSrc).toBeDefined();
|
expect(buttons[0].iconSrc).toBeDefined();
|
||||||
expect(buttons[0].iconAlt).toBe("Create Copy Job");
|
expect(buttons[0].iconAlt).toBe("Create Copy Job");
|
||||||
@@ -148,10 +148,7 @@ describe("CommandBar Utils", () => {
|
|||||||
expect(buttons[1].iconAlt).toBe("Refresh");
|
expect(buttons[1].iconAlt).toBe("Refresh");
|
||||||
|
|
||||||
expect(buttons[2].iconSrc).toBeDefined();
|
expect(buttons[2].iconSrc).toBeDefined();
|
||||||
expect(buttons[2].iconAlt).toBe("Dark Theme");
|
expect(buttons[2].iconAlt).toBe("Feedback");
|
||||||
|
|
||||||
expect(buttons[3].iconSrc).toBeDefined();
|
|
||||||
expect(buttons[3].iconAlt).toBe("Feedback");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
|
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
|
||||||
@@ -160,14 +157,14 @@ describe("CommandBar Utils", () => {
|
|||||||
return selector(state);
|
return selector(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set hasPopup to false for all buttons", () => {
|
it("should set hasPopup to false for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.hasPopup).toBe(false);
|
expect(button.hasPopup).toBe(false);
|
||||||
@@ -175,7 +172,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set commandButtonLabel to undefined for all buttons", () => {
|
it("should set commandButtonLabel to undefined for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.commandButtonLabel).toBeUndefined();
|
expect(button.commandButtonLabel).toBeUndefined();
|
||||||
@@ -183,7 +180,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should respect disabled state when provided", () => {
|
it("should respect disabled state when provided", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.disabled).toBe(false);
|
expect(button.disabled).toBe(false);
|
||||||
@@ -191,7 +188,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return CommandButtonComponentProps with all required properties", () => {
|
it("should return CommandButtonComponentProps with all required properties", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons.forEach((button: CommandButtonComponentProps) => {
|
buttons.forEach((button: CommandButtonComponentProps) => {
|
||||||
expect(button).toHaveProperty("iconSrc");
|
expect(button).toHaveProperty("iconSrc");
|
||||||
@@ -205,19 +202,18 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should maintain button order: create, refresh, themeToggle, feedback", () => {
|
it("should maintain button order: create, refresh, feedback", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
expect(buttons[0].tooltipText).toBe("Create Copy Job");
|
expect(buttons[0].tooltipText).toBe("Create Copy Job");
|
||||||
expect(buttons[1].tooltipText).toBe("Refresh");
|
expect(buttons[1].tooltipText).toBe("Refresh");
|
||||||
expect(buttons[2].tooltipText).toBe("Dark Theme");
|
expect(buttons[2].tooltipText).toBe("Feedback");
|
||||||
expect(buttons[3].tooltipText).toBe("Feedback");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Button click handlers", () => {
|
describe("Button click handlers", () => {
|
||||||
it("should execute click handlers without errors", () => {
|
it("should execute click handlers without errors", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||||
@@ -225,7 +221,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call correct action for each button", () => {
|
it("should call correct action for each button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons[0].onCommandClick({} as React.SyntheticEvent);
|
buttons[0].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
||||||
@@ -233,14 +229,14 @@ describe("CommandBar Utils", () => {
|
|||||||
buttons[1].onCommandClick({} as React.SyntheticEvent);
|
buttons[1].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(mockRefreshJobList).toHaveBeenCalled();
|
expect(mockRefreshJobList).toHaveBeenCalled();
|
||||||
|
|
||||||
buttons[3].onCommandClick({} as React.SyntheticEvent);
|
buttons[2].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
|
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Accessibility", () => {
|
describe("Accessibility", () => {
|
||||||
it("should have aria labels for all buttons", () => {
|
it("should have aria labels for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.ariaLabel).toBeDefined();
|
expect(button.ariaLabel).toBeDefined();
|
||||||
@@ -250,7 +246,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have tooltip text for all buttons", () => {
|
it("should have tooltip text for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.tooltipText).toBeDefined();
|
expect(button.tooltipText).toBeDefined();
|
||||||
@@ -260,7 +256,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have icon alt text for all buttons", () => {
|
it("should have icon alt text for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
const buttons = getCommandBarButtons(mockExplorer);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.iconAlt).toBeDefined();
|
expect(button.iconAlt).toBeDefined();
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import AddIcon from "../../../../images/Add.svg";
|
import AddIcon from "../../../../images/Add.svg";
|
||||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||||
import MoonIcon from "../../../../images/MoonIcon.svg";
|
|
||||||
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
||||||
import SunIcon from "../../../../images/SunIcon.svg";
|
|
||||||
import { configContext, Platform } from "../../../ConfigContext";
|
import { configContext, Platform } from "../../../ConfigContext";
|
||||||
import { useThemeStore } from "../../../hooks/useTheme";
|
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import * as Actions from "../Actions/CopyJobActions";
|
import * as Actions from "../Actions/CopyJobActions";
|
||||||
@@ -12,7 +9,7 @@ import ContainerCopyMessages from "../ContainerCopyMessages";
|
|||||||
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||||
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
||||||
|
|
||||||
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
|
function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
|
||||||
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
|
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
|
||||||
const buttons: CopyJobCommandBarBtnType[] = [
|
const buttons: CopyJobCommandBarBtnType[] = [
|
||||||
{
|
{
|
||||||
@@ -29,15 +26,7 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
|
|||||||
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
||||||
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "themeToggle",
|
|
||||||
iconSrc: isDarkMode ? SunIcon : MoonIcon,
|
|
||||||
label: isDarkMode ? "Light Theme" : "Dark Theme",
|
|
||||||
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme",
|
|
||||||
onClick: () => useThemeStore.getState().toggleTheme(),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (configContext.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
key: "feedback",
|
key: "feedback",
|
||||||
@@ -65,6 +54,6 @@ function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProp
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommandBarButtons(explorer: Explorer, isDarkMode: boolean): CommandButtonComponentProps[] {
|
export function getCommandBarButtons(explorer: Explorer): CommandButtonComponentProps[] {
|
||||||
return getCopyJobBtns(explorer, isDarkMode).map(btnMapper);
|
return getCopyJobBtns(explorer).map(btnMapper);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,18 +25,7 @@ export default {
|
|||||||
subscriptionDropdownPlaceholder: "Select a subscription",
|
subscriptionDropdownPlaceholder: "Select a subscription",
|
||||||
sourceAccountDropdownLabel: "Account",
|
sourceAccountDropdownLabel: "Account",
|
||||||
sourceAccountDropdownPlaceholder: "Select an account",
|
sourceAccountDropdownPlaceholder: "Select an account",
|
||||||
migrationTypeOptions: {
|
migrationTypeCheckboxLabel: "Copy container in offline mode",
|
||||||
offline: {
|
|
||||||
title: "Offline mode",
|
|
||||||
description:
|
|
||||||
"Offline container copy jobs let you copy data from a source container to a destination Cosmos DB container for supported APIs. To ensure data integrity between the source and destination, we recommend stopping updates on the source container before creating the copy job. Learn more about [offline copy jobs](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql).",
|
|
||||||
},
|
|
||||||
online: {
|
|
||||||
title: "Online mode",
|
|
||||||
description:
|
|
||||||
"Online container copy jobs let you copy data from a source container to a destination Cosmos DB NoSQL API container using the [All Versions and Delete](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) change feed. This allows updates to continue on the source while data is copied. A brief downtime is required at the end to safely switch over client applications to the destination container. Learn more about [online copy jobs](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started).",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Select Source and Target Containers Screen
|
// Select Source and Target Containers Screen
|
||||||
selectSourceAndTargetContainersDescription:
|
selectSourceAndTargetContainersDescription:
|
||||||
@@ -184,10 +173,5 @@ export default {
|
|||||||
Skipped: "Cancelled",
|
Skipped: "Cancelled",
|
||||||
Cancelled: "Cancelled",
|
Cancelled: "Cancelled",
|
||||||
},
|
},
|
||||||
dialog: {
|
|
||||||
heading: "",
|
|
||||||
confirmButtonText: "Confirm",
|
|
||||||
cancelButtonText: "Cancel",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,12 +12,7 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const managedIdentityTooltip = (
|
const managedIdentityTooltip = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.addManagedIdentity.tooltip.content}
|
{ContainerCopyMessages.addManagedIdentity.tooltip.content}
|
||||||
<Link
|
<Link href={ContainerCopyMessages.addManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer">
|
||||||
style={{ color: "var(--colorBrandForeground1)" }}
|
|
||||||
href={ContainerCopyMessages.addManagedIdentity.tooltip.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
|
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -31,7 +26,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||||
<Text className="themeText">
|
<Text>
|
||||||
{ContainerCopyMessages.addManagedIdentity.description} 
|
{ContainerCopyMessages.addManagedIdentity.description} 
|
||||||
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer">
|
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer">
|
||||||
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText}
|
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText}
|
||||||
@@ -40,7 +35,6 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
<InfoTooltip content={managedIdentityTooltip} />
|
<InfoTooltip content={managedIdentityTooltip} />
|
||||||
</Text>
|
</Text>
|
||||||
<Toggle
|
<Toggle
|
||||||
data-test="btn-toggle"
|
|
||||||
checked={systemAssigned}
|
checked={systemAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const TooltipContent = (
|
const TooltipContent = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
||||||
<Link
|
<Link href={ContainerCopyMessages.readPermissionAssigned.tooltip.href} target="_blank" rel="noopener noreferrer">
|
||||||
style={{ color: "var(--colorBrandForeground1)" }}
|
|
||||||
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -70,7 +65,6 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
|||||||
<InfoTooltip content={TooltipContent} />
|
<InfoTooltip content={TooltipContent} />
|
||||||
</Text>
|
</Text>
|
||||||
<Toggle
|
<Toggle
|
||||||
data-test="btn-toggle"
|
|
||||||
checked={readPermissionAssigned}
|
checked={readPermissionAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useCopyJobPrerequisitesCache } from "../../Utils/useCopyJobPrerequisite
|
|||||||
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
|
||||||
|
|
||||||
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
|
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
|
||||||
<AccordionItem key={id} value={id} disabled={disabled} data-test="accordion-item">
|
<AccordionItem key={id} value={id} disabled={disabled}>
|
||||||
<AccordionHeader className="accordionHeader">
|
<AccordionHeader className="accordionHeader">
|
||||||
<Text className="accordionHeaderText" variant="medium">
|
<Text className="accordionHeaderText" variant="medium">
|
||||||
{title}
|
{title}
|
||||||
@@ -25,13 +25,13 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Compo
|
|||||||
height={completed ? 20 : 24}
|
height={completed ? 20 : 24}
|
||||||
/>
|
/>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
<AccordionPanel aria-disabled={disabled} className="accordionPanel" data-test="accordion-panel">
|
<AccordionPanel aria-disabled={disabled} className="accordionPanel">
|
||||||
<Component />
|
<Component />
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, description, sections }) => {
|
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description, sections }) => {
|
||||||
const [openItems, setOpenItems] = React.useState<string[]>([]);
|
const [openItems, setOpenItems] = React.useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,12 +44,11 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, descripti
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
data-test={`permission-group-container-${id}`}
|
|
||||||
tokens={{ childrenGap: 15 }}
|
tokens={{ childrenGap: 15 }}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
background: "var(--colorNeutralBackground2)",
|
background: "#fafafa",
|
||||||
border: "1px solid var(--colorNeutralStroke1)",
|
border: "1px solid #e1e1e1",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||||
@@ -57,11 +56,11 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, descripti
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
<Text variant="medium" style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
|
<Text variant="medium" style={{ fontWeight: 600 }}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{description && (
|
{description && (
|
||||||
<Text variant="small" styles={{ root: { color: "var(--colorNeutralForeground2)" } }}>
|
<Text variant="small" styles={{ root: { color: "#605E5C" } }}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -100,12 +99,8 @@ const AssignPermissions = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}>
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
<Text variant="medium">
|
||||||
className="assignPermissionsContainer"
|
|
||||||
tokens={{ childrenGap: 20 }}
|
|
||||||
>
|
|
||||||
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
|
|
||||||
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
||||||
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
||||||
copyJobState?.source?.account?.name || "",
|
copyJobState?.source?.account?.name || "",
|
||||||
|
|||||||
@@ -12,12 +12,7 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const managedIdentityTooltip = (
|
const managedIdentityTooltip = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content}
|
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content}
|
||||||
<Link
|
<Link href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer">
|
||||||
style={{ color: "var(--colorBrandForeground1)" }}
|
|
||||||
href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
|
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -36,7 +31,6 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
<InfoTooltip content={managedIdentityTooltip} />
|
<InfoTooltip content={managedIdentityTooltip} />
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
data-test="btn-toggle"
|
|
||||||
checked={defaultSystemAssigned}
|
checked={defaultSystemAssigned}
|
||||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ import InfoTooltip from "../Components/InfoTooltip";
|
|||||||
const tooltipContent = (
|
const tooltipContent = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}
|
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}
|
||||||
<Link
|
<Link href={ContainerCopyMessages.pointInTimeRestore.tooltip.href} target="_blank" rel="noopener noreferrer">
|
||||||
style={{ color: "var(--colorBrandForeground1)" }}
|
|
||||||
href={ContainerCopyMessages.pointInTimeRestore.tooltip.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
|
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -132,7 +127,6 @@ const PointInTimeRestore: React.FC = () => {
|
|||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
{showRefreshButton ? (
|
{showRefreshButton ? (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
data-test="pointInTimeRestore:RefreshBtn"
|
|
||||||
className="fullWidth"
|
className="fullWidth"
|
||||||
text={ContainerCopyMessages.refreshButtonLabel}
|
text={ContainerCopyMessages.refreshButtonLabel}
|
||||||
iconProps={{ iconName: "Refresh" }}
|
iconProps={{ iconName: "Refresh" }}
|
||||||
@@ -140,7 +134,6 @@ const PointInTimeRestore: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
data-test="pointInTimeRestore:PrimaryBtn"
|
|
||||||
className="fullWidth"
|
className="fullWidth"
|
||||||
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
||||||
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="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.
|
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,7 +67,6 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
|||||||
class="ms-Toggle-background pill-117"
|
class="ms-Toggle-background pill-117"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle1"
|
id="Toggle1"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -93,7 +92,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="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.
|
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.
|
||||||
|
|
||||||
@@ -155,7 +154,6 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
class="ms-Toggle-background pill-121"
|
class="ms-Toggle-background pill-121"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle11"
|
id="Toggle11"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -175,12 +173,10 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground loading css-123"
|
class="ms-Stack popover-container foreground loading css-123"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-135"
|
class="ms-Overlay root-135"
|
||||||
data-test="loading-overlay"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-137"
|
class="ms-Spinner root-137"
|
||||||
@@ -196,13 +192,13 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="themeText css-124"
|
class="css-124"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Enable system assigned managed identity
|
Enable system assigned managed identity
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
>
|
>
|
||||||
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
||||||
</span>
|
</span>
|
||||||
@@ -265,7 +261,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="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.
|
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.
|
||||||
|
|
||||||
@@ -327,7 +323,6 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
class="ms-Toggle-background pill-121"
|
class="ms-Toggle-background pill-121"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle3"
|
id="Toggle3"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -347,17 +342,16 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-123"
|
class="ms-Stack popover-container foreground css-123"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-124"
|
class="css-124"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Enable system assigned managed identity
|
Enable system assigned managed identity
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
>
|
>
|
||||||
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle17"
|
id="Toggle17"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -104,7 +103,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle16"
|
id="Toggle16"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -167,7 +165,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle3"
|
id="Toggle3"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -230,7 +227,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle1"
|
id="Toggle1"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -318,7 +314,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle0"
|
id="Toggle0"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -381,7 +376,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle2"
|
id="Toggle2"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -16,7 +15,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
data-test="permission-group-container-testGroup"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -38,7 +36,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -88,7 +85,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -138,7 +134,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
data-test="accordion-panel"
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -147,7 +142,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
||||||
@@ -207,7 +201,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -219,7 +212,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
data-test="permission-group-container-testGroup"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -241,7 +233,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -291,7 +282,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -341,7 +331,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
data-test="accordion-panel"
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -350,7 +339,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
||||||
@@ -410,7 +398,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -422,7 +409,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
data-test="permission-group-container-testGroup"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -444,7 +430,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -494,7 +479,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -544,7 +528,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
data-test="accordion-panel"
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -553,7 +536,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
||||||
@@ -613,7 +595,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -625,7 +606,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
data-test="permission-group-container-testGroup"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -647,7 +627,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -697,7 +676,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -747,7 +725,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
data-test="accordion-panel"
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Incomplete Component
|
Incomplete Component
|
||||||
@@ -756,7 +733,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
|
||||||
@@ -816,7 +792,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -828,7 +803,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
data-test="permission-group-container-crossAccountConfigs"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -850,7 +824,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -902,7 +875,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
data-test="permission-group-container-onlineConfigs"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -924,7 +896,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -974,7 +945,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
data-test="accordion-panel"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="online-copy-enabled"
|
data-testid="online-copy-enabled"
|
||||||
@@ -994,7 +964,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -1006,7 +975,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
data-test="permission-group-container-onlineConfigs"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -1028,7 +996,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1078,7 +1045,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1128,7 +1094,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
data-test="accordion-panel"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="online-copy-enabled"
|
data-testid="online-copy-enabled"
|
||||||
@@ -1148,7 +1113,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -1160,7 +1124,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-112"
|
class="ms-Stack css-112"
|
||||||
data-test="permission-group-container-crossAccountConfigs"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack css-113"
|
class="ms-Stack css-113"
|
||||||
@@ -1182,7 +1145,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1232,7 +1194,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionItem"
|
class="fui-AccordionItem"
|
||||||
data-test="accordion-item"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
|
||||||
@@ -1282,7 +1243,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
|||||||
<div
|
<div
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
|
||||||
data-test="accordion-panel"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="add-read-permission"
|
data-testid="add-read-permission"
|
||||||
@@ -1302,7 +1262,6 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
@@ -1324,7 +1283,6 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack assignPermissionsContainer css-109"
|
class="ms-Stack assignPermissionsContainer css-109"
|
||||||
data-test="Panel:AssignPermissionsContainer"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="css-110"
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle14"
|
id="Toggle14"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -104,7 +103,6 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle15"
|
id="Toggle15"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -167,7 +165,6 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle10"
|
id="Toggle10"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -259,7 +256,6 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
|
|||||||
class="ms-Toggle-background pill-115"
|
class="ms-Toggle-background pill-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle0"
|
id="Toggle0"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -322,7 +318,6 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
|
|||||||
class="ms-Toggle-background pill-119"
|
class="ms-Toggle-background pill-119"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="btn-toggle"
|
|
||||||
id="Toggle7"
|
id="Toggle7"
|
||||||
role="switch"
|
role="switch"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ exports[`PointInTimeRestore Initial Render should render correctly with default
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-test="pointInTimeRestore:PrimaryBtn"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -132,7 +131,6 @@ exports[`PointInTimeRestore Initial Render should render with empty account name
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-test="pointInTimeRestore:PrimaryBtn"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -163,7 +161,6 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Overlay root-123"
|
class="ms-Overlay root-123"
|
||||||
data-test="loading-overlay"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Spinner root-125"
|
class="ms-Spinner root-125"
|
||||||
@@ -226,7 +223,6 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
|
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
data-test="pointInTimeRestore:PrimaryBtn"
|
|
||||||
disabled=""
|
disabled=""
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@@ -305,7 +301,6 @@ exports[`PointInTimeRestore Snapshots should match snapshot with refresh button
|
|||||||
<button
|
<button
|
||||||
class="ms-Button ms-Button--primary fullWidth root-115"
|
class="ms-Button ms-Button--primary fullWidth root-115"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-test="pointInTimeRestore:RefreshBtn"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -19,21 +19,9 @@ const NavigationControls: React.FC<NavigationControlsProps> = ({
|
|||||||
isPreviousDisabled,
|
isPreviousDisabled,
|
||||||
}) => (
|
}) => (
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
<PrimaryButton
|
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} />
|
||||||
data-test="copy-job-primary"
|
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} />
|
||||||
text={primaryBtnText}
|
<DefaultButton text="Cancel" onClick={onCancel} />
|
||||||
onClick={onPrimary}
|
|
||||||
allowDisabledFocus
|
|
||||||
disabled={isPrimaryDisabled}
|
|
||||||
/>
|
|
||||||
<DefaultButton
|
|
||||||
data-test="copy-job-previous"
|
|
||||||
text="Previous"
|
|
||||||
onClick={onPrevious}
|
|
||||||
allowDisabledFocus
|
|
||||||
disabled={isPreviousDisabled}
|
|
||||||
/>
|
|
||||||
<DefaultButton data-test="copy-job-cancel" text="Cancel" onClick={onCancel} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,15 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
|
|||||||
({ isLoading = false, title, children, onPrimary, onCancel }) => {
|
({ isLoading = false, title, children, onPrimary, onCancel }) => {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
data-test="popover-container"
|
|
||||||
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
|
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
|
||||||
tokens={{ childrenGap: 20 }}
|
tokens={{ childrenGap: 20 }}
|
||||||
style={{ maxWidth: 450 }}
|
style={{ maxWidth: 450 }}
|
||||||
>
|
>
|
||||||
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
|
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
|
||||||
<Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}>
|
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="themeText">{children}</Text>
|
<Text>{children}</Text>
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
|
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
|
||||||
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
|
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -72,11 +71,10 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
@@ -135,11 +133,10 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
@@ -198,17 +195,16 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
This is a very long title that might cause layout issues or text wrapping in the popover component
|
This is a very long title that might cause layout issues or text wrapping in the popover component
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -270,17 +266,16 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -340,17 +335,16 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
@@ -415,17 +409,16 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground css-109"
|
class="ms-Stack popover-container foreground css-109"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Custom Title
|
Custom Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -485,7 +478,6 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack popover-container foreground loading css-109"
|
class="ms-Stack popover-container foreground loading css-109"
|
||||||
data-test="popover-container"
|
|
||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -493,13 +485,13 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
|
|||||||
data-testid="loading-overlay"
|
data-testid="loading-overlay"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
|||||||
return (
|
return (
|
||||||
<Stack className="addCollectionPanelWrapper">
|
<Stack className="addCollectionPanelWrapper">
|
||||||
<Stack.Item className="addCollectionPanelHeader">
|
<Stack.Item className="addCollectionPanelHeader">
|
||||||
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
<Text>{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item className="addCollectionPanelBody">
|
<Stack.Item className="addCollectionPanelBody">
|
||||||
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -50,7 +50,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -91,7 +91,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -132,7 +132,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const CreateCopyJobScreens: React.FC = () => {
|
|||||||
<Stack.Item className="createCopyJobScreensContent">
|
<Stack.Item className="createCopyJobScreensContent">
|
||||||
{contextError && (
|
{contextError && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
data-test="Panel:ErrorContainer"
|
|
||||||
className="createCopyJobErrorMessageBar"
|
className="createCopyJobErrorMessageBar"
|
||||||
messageBarType={MessageBarType.blocked}
|
messageBarType={MessageBarType.blocked}
|
||||||
isMultiline={false}
|
isMultiline={false}
|
||||||
|
|||||||
@@ -31,21 +31,17 @@ const PreviewCopyJob: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
|
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
|
||||||
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
|
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
|
||||||
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
<TextField value={jobName} onChange={onJobNameChange} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||||
<Text data-test="source-subscription-name" className="themeText">
|
<Text>{copyJobState.source?.subscription?.displayName}</Text>
|
||||||
{copyJobState.source?.subscription?.displayName}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||||
<Text data-test="source-account-name" className="themeText">
|
<Text>{copyJobState.source?.account?.name}</Text>
|
||||||
{copyJobState.source?.account?.name}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<DetailsList
|
<DetailsList
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
|
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -33,7 +32,6 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField84"
|
id="TextField84"
|
||||||
type="text"
|
type="text"
|
||||||
value="job-with@special#chars_123"
|
value="job-with@special#chars_123"
|
||||||
@@ -47,13 +45,12 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-subscription-name"
|
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -62,13 +59,12 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-account-name"
|
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -325,7 +321,6 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = `
|
exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -355,7 +350,6 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField96"
|
id="TextField96"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -369,13 +363,12 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-subscription-name"
|
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -384,13 +377,12 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-account-name"
|
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -647,7 +639,6 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
exports[`PreviewCopyJob should render with default state and empty job name 1`] = `
|
exports[`PreviewCopyJob should render with default state and empty job name 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -677,7 +668,6 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField0"
|
id="TextField0"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -691,13 +681,12 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-subscription-name"
|
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -706,13 +695,12 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-account-name"
|
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -969,7 +957,6 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
exports[`PreviewCopyJob should render with long subscription and account names 1`] = `
|
exports[`PreviewCopyJob should render with long subscription and account names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -999,7 +986,6 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField60"
|
id="TextField60"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1013,13 +999,12 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-subscription-name"
|
|
||||||
>
|
>
|
||||||
This is a very long subscription name that might cause display issues if not handled properly
|
This is a very long subscription name that might cause display issues if not handled properly
|
||||||
</span>
|
</span>
|
||||||
@@ -1028,13 +1013,12 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-account-name"
|
|
||||||
>
|
>
|
||||||
this-is-a-very-long-database-account-name-that-might-cause-display-issues
|
this-is-a-very-long-database-account-name-that-might-cause-display-issues
|
||||||
</span>
|
</span>
|
||||||
@@ -1291,7 +1275,6 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
exports[`PreviewCopyJob should render with missing source account information 1`] = `
|
exports[`PreviewCopyJob should render with missing source account information 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1321,7 +1304,6 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField36"
|
id="TextField36"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1335,13 +1317,12 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-subscription-name"
|
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1350,7 +1331,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
@@ -1607,7 +1588,6 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
exports[`PreviewCopyJob should render with missing source subscription information 1`] = `
|
exports[`PreviewCopyJob should render with missing source subscription information 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1637,7 +1617,6 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField24"
|
id="TextField24"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -1651,7 +1630,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1660,13 +1639,12 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-account-name"
|
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -1923,7 +1901,6 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
exports[`PreviewCopyJob should render with online migration type 1`] = `
|
exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -1953,7 +1930,6 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField72"
|
id="TextField72"
|
||||||
type="text"
|
type="text"
|
||||||
value="online-migration-job"
|
value="online-migration-job"
|
||||||
@@ -1967,13 +1943,12 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-subscription-name"
|
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1982,13 +1957,12 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-account-name"
|
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -2245,7 +2219,6 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -2275,7 +2248,6 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField12"
|
id="TextField12"
|
||||||
type="text"
|
type="text"
|
||||||
value="custom-job-name-123"
|
value="custom-job-name-123"
|
||||||
@@ -2289,13 +2261,12 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-subscription-name"
|
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -2304,13 +2275,12 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-account-name"
|
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -2567,7 +2537,6 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
exports[`PreviewCopyJob should render with undefined database and container names 1`] = `
|
exports[`PreviewCopyJob should render with undefined database and container names 1`] = `
|
||||||
<div
|
<div
|
||||||
class="ms-Stack previewCopyJobContainer css-109"
|
class="ms-Stack previewCopyJobContainer css-109"
|
||||||
data-test="Panel:PreviewCopyJob"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack flex-row css-110"
|
class="ms-Stack flex-row css-110"
|
||||||
@@ -2597,7 +2566,6 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
<input
|
<input
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
class="ms-TextField-field field-115"
|
class="ms-TextField-field field-115"
|
||||||
data-test="job-name-textfield"
|
|
||||||
id="TextField48"
|
id="TextField48"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
@@ -2611,13 +2579,12 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-subscription-name"
|
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -2626,13 +2593,12 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="css-125"
|
||||||
data-test="source-account-name"
|
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
|||||||
import { CopyJobContext } from "../../../../Context/CopyJobContext";
|
import { CopyJobContext } from "../../../../Context/CopyJobContext";
|
||||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||||
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||||
import { AccountDropdown, normalizeAccountId } from "./AccountDropdown";
|
import { AccountDropdown } from "./AccountDropdown";
|
||||||
|
|
||||||
jest.mock("../../../../../../hooks/useDatabaseAccounts");
|
jest.mock("../../../../../../hooks/useDatabaseAccounts");
|
||||||
jest.mock("../../../../../../UserContext", () => ({
|
jest.mock("../../../../../../UserContext", () => ({
|
||||||
@@ -202,16 +202,13 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(mockCopyJobState);
|
const newState = stateUpdateFunction(mockCopyJobState);
|
||||||
expect(newState.source.account).toEqual({
|
expect(newState.source.account).toBe(mockDatabaseAccount1);
|
||||||
...mockDatabaseAccount1,
|
|
||||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should auto-select predefined account from userContext if available", async () => {
|
it("should auto-select predefined account from userContext if available", async () => {
|
||||||
const userContextAccount = {
|
const userContextAccount = {
|
||||||
...mockDatabaseAccount2,
|
...mockDatabaseAccount2,
|
||||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account2",
|
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2",
|
||||||
};
|
};
|
||||||
|
|
||||||
(userContext as any).databaseAccount = userContextAccount;
|
(userContext as any).databaseAccount = userContextAccount;
|
||||||
@@ -226,10 +223,7 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(mockCopyJobState);
|
const newState = stateUpdateFunction(mockCopyJobState);
|
||||||
expect(newState.source.account).toEqual({
|
expect(newState.source.account).toBe(mockDatabaseAccount2);
|
||||||
...mockDatabaseAccount2,
|
|
||||||
id: normalizeAccountId(mockDatabaseAccount2.id),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should keep current account if it exists in the filtered list", async () => {
|
it("should keep current account if it exists in the filtered list", async () => {
|
||||||
@@ -254,16 +248,7 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
||||||
expect(newState).toEqual({
|
expect(newState).toBe(contextWithSelectedAccount.copyJobState);
|
||||||
...contextWithSelectedAccount.copyJobState,
|
|
||||||
source: {
|
|
||||||
...contextWithSelectedAccount.copyJobState.source,
|
|
||||||
account: {
|
|
||||||
...mockDatabaseAccount1,
|
|
||||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle account change when user selects different account", async () => {
|
it("should handle account change when user selects different account", async () => {
|
||||||
@@ -287,7 +272,7 @@ describe("AccountDropdown", () => {
|
|||||||
it("should normalize account ID for Portal platform", () => {
|
it("should normalize account ID for Portal platform", () => {
|
||||||
const portalAccount = {
|
const portalAccount = {
|
||||||
...mockDatabaseAccount1,
|
...mockDatabaseAccount1,
|
||||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account1",
|
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account1",
|
||||||
};
|
};
|
||||||
|
|
||||||
(configContext as any).platform = Platform.Portal;
|
(configContext as any).platform = Platform.Portal;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import FieldRow from "../../Components/FieldRow";
|
|||||||
|
|
||||||
interface AccountDropdownProps {}
|
interface AccountDropdownProps {}
|
||||||
|
|
||||||
export const normalizeAccountId = (id: string = "") => {
|
const normalizeAccountId = (id: string) => {
|
||||||
if (configContext.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
|
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
|
||||||
} else if (configContext.platform === Platform.Hosted) {
|
} else if (configContext.platform === Platform.Hosted) {
|
||||||
@@ -27,12 +27,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
|
|
||||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||||
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
||||||
const sqlApiOnlyAccounts = (allAccounts || [])
|
const sqlApiOnlyAccounts: DatabaseAccount[] = (allAccounts || []).filter((account) => apiType(account) === "SQL");
|
||||||
.filter((account) => apiType(account) === "SQL")
|
|
||||||
.map((account) => ({
|
|
||||||
...account,
|
|
||||||
id: normalizeAccountId(account.id),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
||||||
setCopyJobState((prevState) => {
|
setCopyJobState((prevState) => {
|
||||||
@@ -52,7 +47,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
||||||
const currentAccountId = copyJobState?.source?.account?.id;
|
const currentAccountId = copyJobState?.source?.account?.id;
|
||||||
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
|
const predefinedAccountId = userContext.databaseAccount?.id;
|
||||||
const selectedAccountId = currentAccountId || predefinedAccountId;
|
const selectedAccountId = currentAccountId || predefinedAccountId;
|
||||||
|
|
||||||
const targetAccount: DatabaseAccount | null =
|
const targetAccount: DatabaseAccount | null =
|
||||||
@@ -63,7 +58,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
|
|
||||||
const accountOptions =
|
const accountOptions =
|
||||||
sqlApiOnlyAccounts?.map((account) => ({
|
sqlApiOnlyAccounts?.map((account) => ({
|
||||||
key: account.id,
|
key: normalizeAccountId(account.id),
|
||||||
text: account.name,
|
text: account.name,
|
||||||
data: account,
|
data: account,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
|
||||||
import React from "react";
|
|
||||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
|
||||||
import { useCopyJobContext } from "../../../../Context/CopyJobContext";
|
|
||||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
|
||||||
import { MigrationType } from "./MigrationType";
|
|
||||||
|
|
||||||
jest.mock("../../../../Context/CopyJobContext", () => ({
|
|
||||||
useCopyJobContext: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("MigrationType", () => {
|
|
||||||
const mockSetCopyJobState = jest.fn();
|
|
||||||
|
|
||||||
const defaultContextValue = {
|
|
||||||
copyJobState: {
|
|
||||||
jobName: "",
|
|
||||||
migrationType: CopyJobMigrationType.Online,
|
|
||||||
source: {
|
|
||||||
subscription: null as any,
|
|
||||||
account: null as any,
|
|
||||||
databaseId: "",
|
|
||||||
containerId: "",
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
subscriptionId: "",
|
|
||||||
account: null as any,
|
|
||||||
databaseId: "",
|
|
||||||
containerId: "",
|
|
||||||
},
|
|
||||||
sourceReadAccessFromTarget: false,
|
|
||||||
},
|
|
||||||
setCopyJobState: mockSetCopyJobState,
|
|
||||||
flow: { currentScreen: "selectAccount" },
|
|
||||||
setFlow: jest.fn(),
|
|
||||||
contextError: "",
|
|
||||||
setContextError: jest.fn(),
|
|
||||||
explorer: {} as any,
|
|
||||||
resetCopyJobState: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
(useCopyJobContext as jest.Mock).mockReturnValue(defaultContextValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Component Rendering", () => {
|
|
||||||
it("should render migration type component with radio buttons", () => {
|
|
||||||
const { container } = render(<MigrationType />);
|
|
||||||
|
|
||||||
expect(container.querySelector("[data-test='migration-type']")).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole("radiogroup")).toBeInTheDocument();
|
|
||||||
|
|
||||||
const offlineRadio = screen.getByRole("radio", {
|
|
||||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
|
||||||
});
|
|
||||||
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title });
|
|
||||||
|
|
||||||
expect(offlineRadio).toBeInTheDocument();
|
|
||||||
expect(onlineRadio).toBeInTheDocument();
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render with online mode selected by default", () => {
|
|
||||||
render(<MigrationType />);
|
|
||||||
|
|
||||||
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title });
|
|
||||||
const offlineRadio = screen.getByRole("radio", {
|
|
||||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(onlineRadio).toBeChecked();
|
|
||||||
expect(offlineRadio).not.toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render with offline mode selected when state is offline", () => {
|
|
||||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
|
||||||
...defaultContextValue,
|
|
||||||
copyJobState: {
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<MigrationType />);
|
|
||||||
|
|
||||||
const offlineRadio = screen.getByRole("radio", {
|
|
||||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
|
||||||
});
|
|
||||||
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title });
|
|
||||||
|
|
||||||
expect(offlineRadio).toBeChecked();
|
|
||||||
expect(onlineRadio).not.toBeChecked();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Descriptions and Learn More Links", () => {
|
|
||||||
it("should render online description and learn more link when online is selected", () => {
|
|
||||||
const { container } = render(<MigrationType />);
|
|
||||||
|
|
||||||
expect(container.querySelector("[data-test='migration-type-description-online']")).toBeInTheDocument();
|
|
||||||
|
|
||||||
const learnMoreLink = screen.getByRole("link", {
|
|
||||||
name: "online copy jobs",
|
|
||||||
});
|
|
||||||
expect(learnMoreLink).toBeInTheDocument();
|
|
||||||
expect(learnMoreLink).toHaveAttribute(
|
|
||||||
"href",
|
|
||||||
"https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started",
|
|
||||||
);
|
|
||||||
expect(learnMoreLink).toHaveAttribute("target", "_blank");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render offline description and learn more link when offline is selected", () => {
|
|
||||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
|
||||||
...defaultContextValue,
|
|
||||||
copyJobState: {
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { container } = render(<MigrationType />);
|
|
||||||
|
|
||||||
expect(container.querySelector("[data-test='migration-type-description-offline']")).toBeInTheDocument();
|
|
||||||
|
|
||||||
const learnMoreLink = screen.getByRole("link", {
|
|
||||||
name: "offline copy jobs",
|
|
||||||
});
|
|
||||||
expect(learnMoreLink).toBeInTheDocument();
|
|
||||||
expect(learnMoreLink).toHaveAttribute(
|
|
||||||
"href",
|
|
||||||
"https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("User Interactions", () => {
|
|
||||||
it("should call setCopyJobState when offline radio button is clicked", () => {
|
|
||||||
render(<MigrationType />);
|
|
||||||
|
|
||||||
const offlineRadio = screen.getByRole("radio", {
|
|
||||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
|
||||||
});
|
|
||||||
fireEvent.click(offlineRadio);
|
|
||||||
|
|
||||||
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
|
|
||||||
|
|
||||||
const updateFunction = mockSetCopyJobState.mock.calls[0][0];
|
|
||||||
const result = updateFunction(defaultContextValue.copyJobState);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call setCopyJobState when online radio button is clicked", () => {
|
|
||||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
|
||||||
...defaultContextValue,
|
|
||||||
copyJobState: {
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<MigrationType />);
|
|
||||||
|
|
||||||
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title });
|
|
||||||
fireEvent.click(onlineRadio);
|
|
||||||
|
|
||||||
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
|
|
||||||
|
|
||||||
const updateFunction = mockSetCopyJobState.mock.calls[0][0];
|
|
||||||
const result = updateFunction({
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: CopyJobMigrationType.Online,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Accessibility", () => {
|
|
||||||
it("should have proper ARIA attributes", () => {
|
|
||||||
render(<MigrationType />);
|
|
||||||
|
|
||||||
const choiceGroup = screen.getByRole("radiogroup");
|
|
||||||
expect(choiceGroup).toBeInTheDocument();
|
|
||||||
expect(choiceGroup).toHaveAttribute("aria-labelledby", "migrationTypeChoiceGroup");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper radio button labels", () => {
|
|
||||||
render(<MigrationType />);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Edge Cases", () => {
|
|
||||||
it("should handle undefined migration type gracefully", () => {
|
|
||||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
|
||||||
...defaultContextValue,
|
|
||||||
copyJobState: {
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { container } = render(<MigrationType />);
|
|
||||||
|
|
||||||
expect(container.querySelector("[data-test='migration-type']")).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle null copyJobState gracefully", () => {
|
|
||||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
|
||||||
...defaultContextValue,
|
|
||||||
copyJobState: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { container } = render(<MigrationType />);
|
|
||||||
|
|
||||||
expect(container.querySelector("[data-test='migration-type']")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/* eslint-disable react/prop-types */
|
|
||||||
/* eslint-disable react/display-name */
|
|
||||||
import { ChoiceGroup, IChoiceGroupOption, Stack, Text } from "@fluentui/react";
|
|
||||||
import MarkdownRender from "@nteract/markdown";
|
|
||||||
import { useCopyJobContext } from "Explorer/ContainerCopy/Context/CopyJobContext";
|
|
||||||
import React from "react";
|
|
||||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
|
||||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
|
||||||
|
|
||||||
interface MigrationTypeProps {}
|
|
||||||
const options: IChoiceGroupOption[] = [
|
|
||||||
{
|
|
||||||
key: CopyJobMigrationType.Offline,
|
|
||||||
text: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
|
||||||
styles: { root: { width: "33%" } },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: CopyJobMigrationType.Online,
|
|
||||||
text: ContainerCopyMessages.migrationTypeOptions.online.title,
|
|
||||||
styles: { root: { width: "33%" } },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const choiceGroupStyles = {
|
|
||||||
flexContainer: { display: "flex" as const },
|
|
||||||
root: {
|
|
||||||
selectors: {
|
|
||||||
".ms-ChoiceField": {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
".ms-ChoiceField-field:hover .ms-ChoiceFieldLabel": {
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
|
|
||||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
|
||||||
const handleChange = (_ev?: React.FormEvent, option?: IChoiceGroupOption) => {
|
|
||||||
if (option) {
|
|
||||||
setCopyJobState((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
migrationType: option.key as CopyJobMigrationType,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedKey = copyJobState?.migrationType ?? "";
|
|
||||||
const selectedKeyLowercase = selectedKey.toLowerCase() as keyof typeof ContainerCopyMessages.migrationTypeOptions;
|
|
||||||
const selectedKeyContent = ContainerCopyMessages.migrationTypeOptions[selectedKeyLowercase];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack data-test="migration-type" className="migrationTypeContainer">
|
|
||||||
<Stack.Item>
|
|
||||||
<ChoiceGroup
|
|
||||||
selectedKey={selectedKey}
|
|
||||||
options={options}
|
|
||||||
onChange={handleChange}
|
|
||||||
ariaLabelledBy="migrationTypeChoiceGroup"
|
|
||||||
styles={choiceGroupStyles}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
{selectedKeyContent && (
|
|
||||||
<Stack.Item styles={{ root: { marginTop: 10 } }}>
|
|
||||||
<Text
|
|
||||||
variant="small"
|
|
||||||
className="migrationTypeDescription"
|
|
||||||
data-test={`migration-type-description-${selectedKeyLowercase}`}
|
|
||||||
>
|
|
||||||
<MarkdownRender source={selectedKeyContent.description} linkTarget="_blank" />
|
|
||||||
</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
import { MigrationTypeCheckbox } from "./MigrationTypeCheckbox";
|
||||||
|
|
||||||
|
describe("MigrationTypeCheckbox", () => {
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Component Rendering", () => {
|
||||||
|
it("should render with default props (unchecked state)", () => {
|
||||||
|
const { container } = render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
|
||||||
|
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render in checked state", () => {
|
||||||
|
const { container } = render(<MigrationTypeCheckbox checked={true} onChange={mockOnChange} />);
|
||||||
|
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display the correct label text", () => {
|
||||||
|
render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
expect(checkbox).toBeInTheDocument();
|
||||||
|
|
||||||
|
const label = screen.getByText("Copy container in offline mode");
|
||||||
|
expect(label).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have correct accessibility attributes when checked", () => {
|
||||||
|
render(<MigrationTypeCheckbox checked={true} onChange={mockOnChange} />);
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
expect(checkbox).toHaveAttribute("checked");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FluentUI Integration", () => {
|
||||||
|
it("should render FluentUI Checkbox component correctly", () => {
|
||||||
|
render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
expect(checkbox).toBeInTheDocument();
|
||||||
|
expect(checkbox).toHaveAttribute("type", "checkbox");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render FluentUI Stack component correctly", () => {
|
||||||
|
render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
|
||||||
|
|
||||||
|
const stackContainer = document.querySelector(".migrationTypeRow");
|
||||||
|
expect(stackContainer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should apply FluentUI Stack horizontal alignment correctly", () => {
|
||||||
|
const { container } = render(<MigrationTypeCheckbox checked={false} onChange={mockOnChange} />);
|
||||||
|
|
||||||
|
const stackContainer = container.querySelector(".migrationTypeRow");
|
||||||
|
expect(stackContainer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
/* eslint-disable react/display-name */
|
||||||
|
import { Checkbox, Stack } from "@fluentui/react";
|
||||||
|
import React from "react";
|
||||||
|
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||||
|
|
||||||
|
interface MigrationTypeCheckboxProps {
|
||||||
|
checked: boolean;
|
||||||
|
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>
|
||||||
|
));
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`MigrationType Component Rendering should render migration type component with radio buttons 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="ms-Stack migrationTypeContainer css-109"
|
|
||||||
data-test="migration-type"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="ms-StackItem css-110"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="ms-ChoiceFieldGroup root-111"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-labelledby="migrationTypeChoiceGroup"
|
|
||||||
role="radiogroup"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="ms-ChoiceFieldGroup-flexContainer flexContainer-112"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="ms-ChoiceField root-113"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="ms-ChoiceField-wrapper"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="ms-ChoiceField-input input-114"
|
|
||||||
id="ChoiceGroup0-offline"
|
|
||||||
name="ChoiceGroup0"
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
class="ms-ChoiceField-field field-115"
|
|
||||||
for="ChoiceGroup0-offline"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="ms-ChoiceFieldLabel"
|
|
||||||
id="ChoiceGroupLabel1-offline"
|
|
||||||
>
|
|
||||||
Offline mode
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="ms-ChoiceField root-113"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="ms-ChoiceField-wrapper"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
checked=""
|
|
||||||
class="ms-ChoiceField-input input-114"
|
|
||||||
id="ChoiceGroup0-online"
|
|
||||||
name="ChoiceGroup0"
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
class="ms-ChoiceField-field is-checked field-120"
|
|
||||||
for="ChoiceGroup0-online"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="ms-ChoiceFieldLabel"
|
|
||||||
id="ChoiceGroupLabel1-online"
|
|
||||||
>
|
|
||||||
Online mode
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="ms-StackItem css-123"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="migrationTypeDescription css-124"
|
|
||||||
data-test="migration-type-description-online"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="markdown-body "
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Online container copy jobs let you copy data from a source container to a destination Cosmos DB NoSQL API container using the
|
|
||||||
<a
|
|
||||||
href="https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
All Versions and Delete
|
|
||||||
</a>
|
|
||||||
change feed. This allows updates to continue on the source while data is copied. A brief downtime is required at the end to safely switch over client applications to the destination container. Learn more about
|
|
||||||
<a
|
|
||||||
href="https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
online copy jobs
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
|
||||||
|
<div
|
||||||
|
class="ms-Stack migrationTypeRow css-109"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ms-Checkbox is-checked is-enabled root-119"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked=""
|
||||||
|
class="input-111"
|
||||||
|
data-ktp-execute-target="true"
|
||||||
|
id="checkbox-1"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="ms-Checkbox-label label-112"
|
||||||
|
for="checkbox-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ms-Checkbox-checkbox checkbox-120"
|
||||||
|
data-ktp-target="true"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
aria-hidden="true"
|
||||||
|
class="ms-Checkbox-checkmark checkmark-122"
|
||||||
|
data-icon-name="CheckMark"
|
||||||
|
>
|
||||||
|
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="ms-Checkbox-text text-115"
|
||||||
|
>
|
||||||
|
Copy container in offline mode
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`MigrationTypeCheckbox Component Rendering should render with default props (unchecked state) 1`] = `
|
||||||
|
<div
|
||||||
|
class="ms-Stack migrationTypeRow css-109"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ms-Checkbox is-enabled root-110"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="input-111"
|
||||||
|
data-ktp-execute-target="true"
|
||||||
|
id="checkbox-0"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="ms-Checkbox-label label-112"
|
||||||
|
for="checkbox-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ms-Checkbox-checkbox checkbox-113"
|
||||||
|
data-ktp-target="true"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
aria-hidden="true"
|
||||||
|
class="ms-Checkbox-checkmark checkmark-118"
|
||||||
|
data-icon-name="CheckMark"
|
||||||
|
>
|
||||||
|
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="ms-Checkbox-text text-115"
|
||||||
|
>
|
||||||
|
Copy container in offline mode
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||||
@@ -18,8 +18,19 @@ jest.mock("./Components/AccountDropdown", () => ({
|
|||||||
AccountDropdown: jest.fn(() => <div data-testid="account-dropdown">Account Dropdown</div>),
|
AccountDropdown: jest.fn(() => <div data-testid="account-dropdown">Account Dropdown</div>),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("./Components/MigrationType", () => ({
|
jest.mock("./Components/MigrationTypeCheckbox", () => ({
|
||||||
MigrationType: jest.fn(() => <div data-testid="migration-type">Migration Type</div>),
|
MigrationTypeCheckbox: jest.fn(({ checked, onChange }: { checked: boolean; onChange: () => void }) => (
|
||||||
|
<div data-testid="migration-type-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
data-testid="migration-checkbox-input"
|
||||||
|
aria-label="Migration Type Checkbox"
|
||||||
|
/>
|
||||||
|
Copy container in offline mode
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("SelectAccount", () => {
|
describe("SelectAccount", () => {
|
||||||
@@ -72,7 +83,7 @@ describe("SelectAccount", () => {
|
|||||||
|
|
||||||
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
|
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();
|
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
expect(screen.getByTestId("migration-type-checkbox")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render correctly with snapshot", () => {
|
it("should render correctly with snapshot", () => {
|
||||||
@@ -82,20 +93,78 @@ describe("SelectAccount", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Migration Type Functionality", () => {
|
describe("Migration Type Functionality", () => {
|
||||||
it("should render migration type component", () => {
|
it("should display migration type checkbox as unchecked when migrationType is Online", () => {
|
||||||
|
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||||
|
...defaultContextValue,
|
||||||
|
copyJobState: {
|
||||||
|
...defaultContextValue.copyJobState,
|
||||||
|
migrationType: CopyJobMigrationType.Online,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
render(<SelectAccount />);
|
render(<SelectAccount />);
|
||||||
|
|
||||||
const migrationTypeComponent = screen.getByTestId("migration-type");
|
const checkbox = screen.getByTestId("migration-checkbox-input");
|
||||||
expect(migrationTypeComponent).toBeInTheDocument();
|
expect(checkbox).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display migration type checkbox as checked when migrationType is Offline", () => {
|
||||||
|
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||||
|
...defaultContextValue,
|
||||||
|
copyJobState: {
|
||||||
|
...defaultContextValue.copyJobState,
|
||||||
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SelectAccount />);
|
||||||
|
|
||||||
|
const checkbox = screen.getByTestId("migration-checkbox-input");
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setCopyJobState with Online migration type when checkbox is unchecked", () => {
|
||||||
|
(useCopyJobContext as jest.Mock).mockReturnValue({
|
||||||
|
...defaultContextValue,
|
||||||
|
copyJobState: {
|
||||||
|
...defaultContextValue.copyJobState,
|
||||||
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SelectAccount />);
|
||||||
|
|
||||||
|
const checkbox = screen.getByTestId("migration-checkbox-input");
|
||||||
|
fireEvent.click(checkbox);
|
||||||
|
|
||||||
|
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
|
||||||
|
|
||||||
|
const updateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
|
const previousState = {
|
||||||
|
...defaultContextValue.copyJobState,
|
||||||
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
|
};
|
||||||
|
const result = updateFunction(previousState);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
...previousState,
|
||||||
|
migrationType: CopyJobMigrationType.Online,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Performance and Optimization", () => {
|
describe("Performance and Optimization", () => {
|
||||||
it("should render without performance issues", () => {
|
it("should maintain referential equality of handler functions between renders", async () => {
|
||||||
const { rerender } = render(<SelectAccount />);
|
const { rerender } = render(<SelectAccount />);
|
||||||
|
|
||||||
|
const migrationCheckbox = (await import("./Components/MigrationTypeCheckbox")).MigrationTypeCheckbox as jest.Mock;
|
||||||
|
const firstRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
|
||||||
|
|
||||||
rerender(<SelectAccount />);
|
rerender(<SelectAccount />);
|
||||||
|
|
||||||
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
const secondRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
|
||||||
|
|
||||||
|
expect(firstRenderHandler).toBe(secondRenderHandler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,33 @@
|
|||||||
import { Stack, Text } from "@fluentui/react";
|
import { Stack, Text } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
|
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||||
import { AccountDropdown } from "./Components/AccountDropdown";
|
import { AccountDropdown } from "./Components/AccountDropdown";
|
||||||
import { MigrationType } from "./Components/MigrationType";
|
import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
|
||||||
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
|
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
|
||||||
|
|
||||||
const SelectAccount = React.memo(() => {
|
const SelectAccount = React.memo(() => {
|
||||||
|
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||||
|
|
||||||
|
const handleMigrationTypeChange = (_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
|
||||||
|
setCopyJobState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrationTypeChecked = copyJobState?.migrationType === CopyJobMigrationType.Offline;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
|
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
|
||||||
<Text className="themeText">{ContainerCopyMessages.selectAccountDescription}</Text>
|
<Text>{ContainerCopyMessages.selectAccountDescription}</Text>
|
||||||
|
|
||||||
<SubscriptionDropdown />
|
<SubscriptionDropdown />
|
||||||
|
|
||||||
<AccountDropdown />
|
<AccountDropdown />
|
||||||
|
|
||||||
<MigrationType />
|
<MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
|||||||
data-test="Panel:SelectAccountContainer"
|
data-test="Panel:SelectAccountContainer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="css-110"
|
||||||
>
|
>
|
||||||
Please select a source account from which to copy.
|
Please select a source account from which to copy.
|
||||||
</span>
|
</span>
|
||||||
@@ -21,9 +21,14 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
|||||||
Account Dropdown
|
Account Dropdown
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-testid="migration-type"
|
data-testid="migration-type-checkbox"
|
||||||
>
|
>
|
||||||
Migration Type
|
<input
|
||||||
|
aria-label="Migration Type Checkbox"
|
||||||
|
data-testid="migration-checkbox-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
Copy container in offline mode
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -47,12 +47,8 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
|
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
|
||||||
data-test="Panel:SelectSourceAndTargetContainers"
|
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
||||||
className="selectSourceAndTargetContainers"
|
|
||||||
tokens={{ childrenGap: 25 }}
|
|
||||||
>
|
|
||||||
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
|
||||||
<DatabaseContainerSection
|
<DatabaseContainerSection
|
||||||
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
||||||
databaseOptions={sourceDatabaseOptions}
|
databaseOptions={sourceDatabaseOptions}
|
||||||
@@ -63,7 +59,6 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
selectedContainer={source?.containerId}
|
selectedContainer={source?.containerId}
|
||||||
containerDisabled={!source?.databaseId}
|
containerDisabled={!source?.databaseId}
|
||||||
containerOnChange={onDropdownChange("sourceContainer")}
|
containerOnChange={onDropdownChange("sourceContainer")}
|
||||||
sectionType="source"
|
|
||||||
/>
|
/>
|
||||||
<DatabaseContainerSection
|
<DatabaseContainerSection
|
||||||
heading={ContainerCopyMessages.targetContainerSubHeading}
|
heading={ContainerCopyMessages.targetContainerSubHeading}
|
||||||
@@ -76,7 +71,6 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
containerDisabled={!target?.databaseId}
|
containerDisabled={!target?.databaseId}
|
||||||
containerOnChange={onDropdownChange("targetContainer")}
|
containerOnChange={onDropdownChange("targetContainer")}
|
||||||
handleOnDemandCreateContainer={showAddCollectionPanel}
|
handleOnDemandCreateContainer={showAddCollectionPanel}
|
||||||
sectionType="target"
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ describe("DatabaseContainerSection", () => {
|
|||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerDisabled: false,
|
containerDisabled: false,
|
||||||
containerOnChange: mockContainerOnChange,
|
containerOnChange: mockContainerOnChange,
|
||||||
sectionType: "source",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -293,7 +292,6 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: mockContainerOptions,
|
containerOptions: mockContainerOptions,
|
||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerOnChange: mockContainerOnChange,
|
containerOnChange: mockContainerOnChange,
|
||||||
sectionType: "source",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<DatabaseContainerSection {...minimalProps} />);
|
render(<DatabaseContainerSection {...minimalProps} />);
|
||||||
@@ -395,7 +393,6 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
|
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
|
||||||
selectedContainer: "c1",
|
selectedContainer: "c1",
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
sectionType: "source",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
|
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
|
||||||
@@ -414,7 +411,6 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerDisabled: false,
|
containerDisabled: false,
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
handleOnDemandCreateContainer: jest.fn(),
|
handleOnDemandCreateContainer: jest.fn(),
|
||||||
sectionType: "target",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...fullProps} />);
|
const { container } = render(<DatabaseContainerSection {...fullProps} />);
|
||||||
@@ -432,7 +428,6 @@ describe("DatabaseContainerSection", () => {
|
|||||||
selectedContainer: "container1",
|
selectedContainer: "container1",
|
||||||
containerDisabled: true,
|
containerDisabled: true,
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
sectionType: "target",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
|
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
|
||||||
@@ -448,7 +443,6 @@ describe("DatabaseContainerSection", () => {
|
|||||||
containerOptions: [],
|
containerOptions: [],
|
||||||
selectedContainer: "",
|
selectedContainer: "",
|
||||||
containerOnChange: jest.fn(),
|
containerOnChange: jest.fn(),
|
||||||
sectionType: "target",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);
|
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const DatabaseContainerSection = ({
|
|||||||
containerDisabled,
|
containerDisabled,
|
||||||
containerOnChange,
|
containerOnChange,
|
||||||
handleOnDemandCreateContainer,
|
handleOnDemandCreateContainer,
|
||||||
sectionType,
|
|
||||||
}: DatabaseContainerSectionProps) => (
|
}: DatabaseContainerSectionProps) => (
|
||||||
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
|
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
|
||||||
<label className="subHeading">{heading}</label>
|
<label className="subHeading">{heading}</label>
|
||||||
@@ -28,7 +27,6 @@ export const DatabaseContainerSection = ({
|
|||||||
disabled={!!databaseDisabled}
|
disabled={!!databaseDisabled}
|
||||||
selectedKey={selectedDatabase}
|
selectedKey={selectedDatabase}
|
||||||
onChange={databaseOnChange}
|
onChange={databaseOnChange}
|
||||||
data-test={`${sectionType}-databaseDropdown`}
|
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
|
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
|
||||||
@@ -41,14 +39,9 @@ export const DatabaseContainerSection = ({
|
|||||||
disabled={!!containerDisabled}
|
disabled={!!containerDisabled}
|
||||||
selectedKey={selectedContainer}
|
selectedKey={selectedContainer}
|
||||||
onChange={containerOnChange}
|
onChange={containerOnChange}
|
||||||
data-test={`${sectionType}-containerDropdown`}
|
|
||||||
/>
|
/>
|
||||||
{handleOnDemandCreateContainer && (
|
{handleOnDemandCreateContainer && (
|
||||||
<ActionButton
|
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
|
||||||
className="create-container-link-btn"
|
|
||||||
style={{ color: "var(--colorBrandForeground1)" }}
|
|
||||||
onClick={() => handleOnDemandCreateContainer()}
|
|
||||||
>
|
|
||||||
{ContainerCopyMessages.createContainerButtonLabel}
|
{ContainerCopyMessages.createContainerButtonLabel}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="target-databaseDropdown"
|
|
||||||
id="Dropdown98"
|
id="Dropdown98"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -95,7 +94,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="target-containerDropdown"
|
|
||||||
id="Dropdown99"
|
id="Dropdown99"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -184,7 +182,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
|
|||||||
class="ms-Dropdown is-disabled is-required dropdown-143"
|
class="ms-Dropdown is-disabled is-required dropdown-143"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="target-databaseDropdown"
|
|
||||||
id="Dropdown103"
|
id="Dropdown103"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -242,7 +239,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
|
|||||||
class="ms-Dropdown is-disabled is-required dropdown-143"
|
class="ms-Dropdown is-disabled is-required dropdown-143"
|
||||||
data-is-focusable="false"
|
data-is-focusable="false"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="target-containerDropdown"
|
|
||||||
id="Dropdown104"
|
id="Dropdown104"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -310,7 +306,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="target-databaseDropdown"
|
|
||||||
id="Dropdown105"
|
id="Dropdown105"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -368,7 +363,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="target-containerDropdown"
|
|
||||||
id="Dropdown106"
|
id="Dropdown106"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -436,7 +430,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="source-databaseDropdown"
|
|
||||||
id="Dropdown96"
|
id="Dropdown96"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -494,7 +487,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
|
|||||||
class="ms-Dropdown is-required dropdown-112"
|
class="ms-Dropdown is-required dropdown-112"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
data-ktp-target="true"
|
data-ktp-target="true"
|
||||||
data-test="source-containerDropdown"
|
|
||||||
id="Dropdown97"
|
id="Dropdown97"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable jest/no-conditional-expect */
|
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -6,20 +5,6 @@ import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../E
|
|||||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||||
|
|
||||||
const mockShowOkCancelModalDialog = jest.fn();
|
|
||||||
const mockCloseDialog = jest.fn();
|
|
||||||
const mockOpenDialog = jest.fn();
|
|
||||||
|
|
||||||
jest.mock("../../../Controls/Dialog", () => ({
|
|
||||||
useDialog: {
|
|
||||||
getState: () => ({
|
|
||||||
showOkCancelModalDialog: mockShowOkCancelModalDialog,
|
|
||||||
closeDialog: mockCloseDialog,
|
|
||||||
openDialog: mockOpenDialog,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../ContainerCopyMessages", () => ({
|
jest.mock("../../ContainerCopyMessages", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: {
|
default: {
|
||||||
@@ -33,11 +18,6 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
|||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
complete: "Complete",
|
complete: "Complete",
|
||||||
},
|
},
|
||||||
dialog: {
|
|
||||||
heading: "Confirm Action",
|
|
||||||
confirmButtonText: "Confirm",
|
|
||||||
cancelButtonText: "Cancel",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -70,9 +50,6 @@ describe("CopyJobActionMenu", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockShowOkCancelModalDialog.mockClear();
|
|
||||||
mockCloseDialog.mockClear();
|
|
||||||
mockOpenDialog.mockClear();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Component Rendering", () => {
|
describe("Component Rendering", () => {
|
||||||
@@ -289,29 +266,7 @@ describe("CopyJobActionMenu", () => {
|
|||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show confirmation dialog when cancel action is clicked", () => {
|
it("should call handleClick when cancel action is clicked", () => {
|
||||||
const job = createMockJob({ Name: "Test Job", Status: CopyJobStatusType.InProgress });
|
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const cancelButton = screen.getByText("Cancel");
|
|
||||||
fireEvent.click(cancelButton);
|
|
||||||
|
|
||||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
|
||||||
"Confirm Action",
|
|
||||||
null,
|
|
||||||
"Confirm",
|
|
||||||
expect.any(Function),
|
|
||||||
"Cancel",
|
|
||||||
null,
|
|
||||||
expect.any(Object), // dialogBody content
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call handleClick when dialog is confirmed for cancel action", () => {
|
|
||||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||||
@@ -322,9 +277,6 @@ describe("CopyJobActionMenu", () => {
|
|||||||
const cancelButton = screen.getByText("Cancel");
|
const cancelButton = screen.getByText("Cancel");
|
||||||
fireEvent.click(cancelButton);
|
fireEvent.click(cancelButton);
|
||||||
|
|
||||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
|
||||||
onOkCallback();
|
|
||||||
|
|
||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -342,33 +294,7 @@ describe("CopyJobActionMenu", () => {
|
|||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
|
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show confirmation dialog when complete action is clicked", () => {
|
it("should call handleClick when complete action is clicked", () => {
|
||||||
const job = createMockJob({
|
|
||||||
Name: "Test Online Job",
|
|
||||||
Status: CopyJobStatusType.InProgress,
|
|
||||||
Mode: CopyJobMigrationType.Online,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const completeButton = screen.getByText("Complete");
|
|
||||||
fireEvent.click(completeButton);
|
|
||||||
|
|
||||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
|
||||||
"Confirm Action",
|
|
||||||
null,
|
|
||||||
"Confirm",
|
|
||||||
expect.any(Function),
|
|
||||||
"Cancel",
|
|
||||||
null,
|
|
||||||
expect.any(Object), // dialogBody content
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call handleClick when dialog is confirmed for complete action", () => {
|
|
||||||
const job = createMockJob({
|
const job = createMockJob({
|
||||||
Status: CopyJobStatusType.InProgress,
|
Status: CopyJobStatusType.InProgress,
|
||||||
Mode: CopyJobMigrationType.Online,
|
Mode: CopyJobMigrationType.Online,
|
||||||
@@ -382,87 +308,10 @@ describe("CopyJobActionMenu", () => {
|
|||||||
const completeButton = screen.getByText("Complete");
|
const completeButton = screen.getByText("Complete");
|
||||||
fireEvent.click(completeButton);
|
fireEvent.click(completeButton);
|
||||||
|
|
||||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
|
||||||
onOkCallback();
|
|
||||||
|
|
||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Dialog Body Content", () => {
|
|
||||||
it("should pass correct dialog body content for cancel action", () => {
|
|
||||||
const job = createMockJob({ Name: "MyTestJob", Status: CopyJobStatusType.InProgress });
|
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const cancelButton = screen.getByText("Cancel");
|
|
||||||
fireEvent.click(cancelButton);
|
|
||||||
|
|
||||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
|
||||||
"Confirm Action",
|
|
||||||
null,
|
|
||||||
"Confirm",
|
|
||||||
expect.any(Function),
|
|
||||||
"Cancel",
|
|
||||||
null,
|
|
||||||
expect.objectContaining({
|
|
||||||
props: expect.objectContaining({
|
|
||||||
tokens: expect.any(Object),
|
|
||||||
children: expect.any(Array),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should pass correct dialog body content for complete action", () => {
|
|
||||||
const job = createMockJob({
|
|
||||||
Name: "OnlineTestJob",
|
|
||||||
Status: CopyJobStatusType.InProgress,
|
|
||||||
Mode: CopyJobMigrationType.Online,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const completeButton = screen.getByText("Complete");
|
|
||||||
fireEvent.click(completeButton);
|
|
||||||
|
|
||||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
|
||||||
"Confirm Action",
|
|
||||||
null,
|
|
||||||
"Confirm",
|
|
||||||
expect.any(Function),
|
|
||||||
"Cancel",
|
|
||||||
null,
|
|
||||||
expect.objectContaining({
|
|
||||||
props: expect.objectContaining({
|
|
||||||
tokens: expect.any(Object),
|
|
||||||
children: expect.any(Array),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not show dialog body for actions without confirmation", () => {
|
|
||||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const pauseButton = screen.getByText("Pause");
|
|
||||||
fireEvent.click(pauseButton);
|
|
||||||
|
|
||||||
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Disabled States During Updates", () => {
|
describe("Disabled States During Updates", () => {
|
||||||
const TestComponentWrapper: React.FC<{
|
const TestComponentWrapper: React.FC<{
|
||||||
job: CopyJobType;
|
job: CopyJobType;
|
||||||
@@ -490,13 +339,8 @@ describe("CopyJobActionMenu", () => {
|
|||||||
const pauseButton = screen.getByText("Pause");
|
const pauseButton = screen.getByText("Pause");
|
||||||
fireEvent.click(pauseButton);
|
fireEvent.click(pauseButton);
|
||||||
fireEvent.click(actionButton);
|
fireEvent.click(actionButton);
|
||||||
|
const pauseButtonAfterClick = screen.getByText("Pause");
|
||||||
const pauseButtonAfterClick = screen.getByText("Pause").closest("button");
|
|
||||||
expect(pauseButtonAfterClick).toBeInTheDocument();
|
expect(pauseButtonAfterClick).toBeInTheDocument();
|
||||||
expect(pauseButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
|
||||||
|
|
||||||
const cancelButtonAfterClick = screen.getByText("Cancel").closest("button");
|
|
||||||
expect(cancelButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not disable actions for different jobs when one is updating", () => {
|
it("should not disable actions for different jobs when one is updating", () => {
|
||||||
@@ -516,7 +360,23 @@ describe("CopyJobActionMenu", () => {
|
|||||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should disable complete action when job is being updated", () => {
|
it("should properly handle multiple action types being disabled for the same job", () => {
|
||||||
|
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||||
|
render(<TestComponentWrapper job={job} />);
|
||||||
|
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||||
|
|
||||||
|
fireEvent.click(actionButton);
|
||||||
|
fireEvent.click(screen.getByText("Pause"));
|
||||||
|
|
||||||
|
fireEvent.click(actionButton);
|
||||||
|
fireEvent.click(screen.getByText("Cancel"));
|
||||||
|
|
||||||
|
fireEvent.click(actionButton);
|
||||||
|
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complete action disabled state for online jobs", () => {
|
||||||
const job = createMockJob({
|
const job = createMockJob({
|
||||||
Status: CopyJobStatusType.InProgress,
|
Status: CopyJobStatusType.InProgress,
|
||||||
Mode: CopyJobMigrationType.Online,
|
Mode: CopyJobMigrationType.Online,
|
||||||
@@ -530,34 +390,8 @@ describe("CopyJobActionMenu", () => {
|
|||||||
const completeButton = screen.getByText("Complete");
|
const completeButton = screen.getByText("Complete");
|
||||||
fireEvent.click(completeButton);
|
fireEvent.click(completeButton);
|
||||||
|
|
||||||
// Simulate dialog confirmation to trigger state update
|
|
||||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
|
||||||
onOkCallback();
|
|
||||||
|
|
||||||
fireEvent.click(actionButton);
|
fireEvent.click(actionButton);
|
||||||
const completeButtonAfterClick = screen.getByText("Complete").closest("button");
|
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||||
expect(completeButtonAfterClick).toBeInTheDocument();
|
|
||||||
expect(completeButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should disable complete action when any other action is being performed", () => {
|
|
||||||
const job = createMockJob({
|
|
||||||
Status: CopyJobStatusType.InProgress,
|
|
||||||
Mode: CopyJobMigrationType.Online,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<TestComponentWrapper job={job} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const pauseButton = screen.getByText("Pause");
|
|
||||||
fireEvent.click(pauseButton);
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const completeButtonAfterClick = screen.getByText("Complete").closest("button");
|
|
||||||
expect(completeButtonAfterClick).toBeInTheDocument();
|
|
||||||
expect(completeButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -628,7 +462,6 @@ describe("CopyJobActionMenu", () => {
|
|||||||
|
|
||||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||||
expect(actionButton).toHaveAttribute("role", "button");
|
|
||||||
|
|
||||||
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
||||||
expect(moreIcon || actionButton).toBeInTheDocument();
|
expect(moreIcon || actionButton).toBeInTheDocument();
|
||||||
@@ -775,129 +608,4 @@ describe("CopyJobActionMenu", () => {
|
|||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Complete Coverage Tests", () => {
|
|
||||||
it("should handle all possible dialog scenarios", () => {
|
|
||||||
const dialogTests = [
|
|
||||||
{ action: CopyJobActions.cancel, status: CopyJobStatusType.InProgress, shouldShowDialog: true },
|
|
||||||
{
|
|
||||||
action: CopyJobActions.complete,
|
|
||||||
status: CopyJobStatusType.InProgress,
|
|
||||||
mode: CopyJobMigrationType.Online,
|
|
||||||
shouldShowDialog: true,
|
|
||||||
},
|
|
||||||
{ action: CopyJobActions.pause, status: CopyJobStatusType.InProgress, shouldShowDialog: false },
|
|
||||||
{ action: CopyJobActions.resume, status: CopyJobStatusType.Paused, shouldShowDialog: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
dialogTests.forEach(({ action, status, mode = CopyJobMigrationType.Offline, shouldShowDialog }, index) => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
const job = createMockJob({ Status: status, Mode: mode, Name: `DialogTestJob${index}` });
|
|
||||||
const { unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const actionText = action.charAt(0).toUpperCase() + action.slice(1);
|
|
||||||
if (screen.queryByText(actionText)) {
|
|
||||||
fireEvent.click(screen.getByText(actionText));
|
|
||||||
|
|
||||||
if (shouldShowDialog) {
|
|
||||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
|
|
||||||
} else {
|
|
||||||
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
|
|
||||||
expect(mockHandleClick).toHaveBeenCalled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unmount();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should verify component handles state updates correctly", () => {
|
|
||||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
|
||||||
const stateUpdater = jest.fn();
|
|
||||||
|
|
||||||
const testHandleClick: HandleJobActionClickType = (job, action, setUpdatingJobAction) => {
|
|
||||||
setUpdatingJobAction({ jobName: job.Name, action });
|
|
||||||
stateUpdater(job.Name, action);
|
|
||||||
};
|
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={testHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const pauseButton = screen.getByText("Pause");
|
|
||||||
fireEvent.click(pauseButton);
|
|
||||||
|
|
||||||
expect(stateUpdater).toHaveBeenCalledWith(job.Name, CopyJobActions.pause);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Full Integration Coverage", () => {
|
|
||||||
it("should test complete workflow for cancel action with dialog", () => {
|
|
||||||
const job = createMockJob({ Name: "Integration Test Job", Status: CopyJobStatusType.InProgress });
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
expect(actionButton).toHaveAttribute("data-test", "CopyJobActionMenu/Button:Integration Test Job");
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const cancelButton = screen.getByText("Cancel");
|
|
||||||
fireEvent.click(cancelButton);
|
|
||||||
|
|
||||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
|
||||||
"Confirm Action", // title
|
|
||||||
null, // subText
|
|
||||||
"Confirm", // confirmLabel
|
|
||||||
expect.any(Function), // onOk
|
|
||||||
"Cancel", // cancelLabel
|
|
||||||
null, // onCancel
|
|
||||||
expect.any(Object), // contentHtml (dialogBody)
|
|
||||||
);
|
|
||||||
|
|
||||||
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
|
|
||||||
onOkCallback();
|
|
||||||
|
|
||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should test complete workflow for complete action with dialog", () => {
|
|
||||||
const job = createMockJob({
|
|
||||||
Name: "Online Integration Job",
|
|
||||||
Status: CopyJobStatusType.Running,
|
|
||||||
Mode: CopyJobMigrationType.Online,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
|
||||||
fireEvent.click(actionButton);
|
|
||||||
|
|
||||||
const completeButton = screen.getByText("Complete");
|
|
||||||
fireEvent.click(completeButton);
|
|
||||||
|
|
||||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
|
|
||||||
|
|
||||||
const dialogContent = mockShowOkCancelModalDialog.mock.calls[0][6];
|
|
||||||
expect(dialogContent).toBeTruthy();
|
|
||||||
|
|
||||||
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
|
|
||||||
onOkCallback();
|
|
||||||
|
|
||||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should maintain proper component lifecycle", () => {
|
|
||||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
|
||||||
const { rerender, unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
|
|
||||||
rerender(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
|
||||||
expect(screen.getByRole("button", { name: "Actions" })).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(() => unmount()).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
|
import { IconButton, IContextualMenuProps } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDialog } from "../../../Controls/Dialog";
|
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||||
@@ -10,28 +9,6 @@ interface CopyJobActionMenuProps {
|
|||||||
handleClick: HandleJobActionClickType;
|
handleClick: HandleJobActionClickType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogBody = {
|
|
||||||
[CopyJobActions.cancel]: (jobName: string) => (
|
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
|
||||||
<Stack.Item>
|
|
||||||
You are about to cancel <b>{jobName}</b> copy job.
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>Cancelling will stop the job immediately.</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
[CopyJobActions.complete]: (jobName: string) => (
|
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
|
||||||
<Stack.Item>
|
|
||||||
You are about to complete <b>{jobName}</b> copy job.
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>
|
|
||||||
Once completed, continuous data copy will stop after any pending documents are processed. To maintain data
|
|
||||||
integrity, we recommend stopping updates to the source container before completing the job.
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
||||||
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
||||||
if (
|
if (
|
||||||
@@ -45,22 +22,9 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showActionConfirmationDialog = (job: CopyJobType, action: CopyJobActions): void => {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkCancelModalDialog(
|
|
||||||
ContainerCopyMessages.MonitorJobs.dialog.heading,
|
|
||||||
null,
|
|
||||||
ContainerCopyMessages.MonitorJobs.dialog.confirmButtonText,
|
|
||||||
() => handleClick(job, action, setUpdatingJobAction),
|
|
||||||
ContainerCopyMessages.MonitorJobs.dialog.cancelButtonText,
|
|
||||||
null,
|
|
||||||
action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMenuItems = (): IContextualMenuProps["items"] => {
|
const getMenuItems = (): IContextualMenuProps["items"] => {
|
||||||
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
||||||
|
const updatingAction = updatingJobAction?.action;
|
||||||
|
|
||||||
const baseItems = [
|
const baseItems = [
|
||||||
{
|
{
|
||||||
@@ -68,21 +32,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
||||||
iconProps: { iconName: "Pause" },
|
iconProps: { iconName: "Pause" },
|
||||||
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
||||||
disabled: isThisJobUpdating,
|
disabled: isThisJobUpdating && updatingAction === CopyJobActions.pause,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: CopyJobActions.cancel,
|
key: CopyJobActions.cancel,
|
||||||
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
||||||
iconProps: { iconName: "Cancel" },
|
iconProps: { iconName: "Cancel" },
|
||||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
|
onClick: () => handleClick(job, CopyJobActions.cancel, setUpdatingJobAction),
|
||||||
disabled: isThisJobUpdating,
|
disabled: isThisJobUpdating && updatingAction === CopyJobActions.cancel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: CopyJobActions.resume,
|
key: CopyJobActions.resume,
|
||||||
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
||||||
iconProps: { iconName: "Play" },
|
iconProps: { iconName: "Play" },
|
||||||
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
|
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
|
||||||
disabled: isThisJobUpdating,
|
disabled: isThisJobUpdating && updatingAction === CopyJobActions.resume,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -103,8 +67,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
key: CopyJobActions.complete,
|
key: CopyJobActions.complete,
|
||||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||||
iconProps: { iconName: "CheckMark" },
|
iconProps: { iconName: "CheckMark" },
|
||||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
onClick: () => handleClick(job, CopyJobActions.complete, setUpdatingJobAction),
|
||||||
disabled: isThisJobUpdating,
|
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return filteredItems;
|
return filteredItems;
|
||||||
@@ -119,11 +83,10 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
data-test={`CopyJobActionMenu/Button:${job.Name}`}
|
|
||||||
role="button"
|
role="button"
|
||||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||||
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
|
menuProps={{ items: getMenuItems() }}
|
||||||
menuIconProps={{ iconName: "", className: "hidden" }}
|
menuIconProps={{ iconName: "" }}
|
||||||
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||||
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
|
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
|
||||||
import React, { memo } from "react";
|
import React, { memo } from "react";
|
||||||
import { useThemeStore } from "../../../../hooks/useTheme";
|
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
import { CopyJobType } from "../../Types/CopyJobTypes";
|
import { CopyJobType } from "../../Types/CopyJobTypes";
|
||||||
@@ -64,19 +63,6 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
||||||
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
|
||||||
|
|
||||||
const errorMessageStyle: React.CSSProperties = {
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
...(isDarkMode && {
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
backgroundColor: "var(--colorNeutralBackground2)",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
padding: "10px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedContainers = [
|
const selectedContainers = [
|
||||||
{
|
{
|
||||||
sourceContainerName: job?.Source?.containerName || "N/A",
|
sourceContainerName: job?.Source?.containerName || "N/A",
|
||||||
@@ -91,10 +77,10 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
|||||||
<Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details">
|
<Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details">
|
||||||
{job.Error ? (
|
{job.Error ? (
|
||||||
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
|
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold themeText" style={sectionCss.headingText}>
|
<Text className="bold" style={sectionCss.headingText}>
|
||||||
{ContainerCopyMessages.errorTitle}
|
{ContainerCopyMessages.errorTitle}
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="pre" style={errorMessageStyle}>
|
<Text as="pre" style={{ whiteSpace: "pre-wrap" }}>
|
||||||
{job.Error.message}
|
{job.Error.message}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@@ -102,16 +88,16 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
|||||||
<Stack.Item data-testid="selectedcollection-stack">
|
<Stack.Item data-testid="selectedcollection-stack">
|
||||||
<Stack tokens={{ childrenGap: 15 }}>
|
<Stack tokens={{ childrenGap: 15 }}>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
|
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
|
||||||
<Text className="themeText">{job.LastUpdatedTime}</Text>
|
<Text>{job.LastUpdatedTime}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||||
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
|
<Text>{job.Source?.remoteAccountName}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
||||||
<Text className="themeText">{job.Mode}</Text>
|
<Text>{job.Mode}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
import { FontIcon, getTheme, mergeStyles, mergeStyleSets, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
|
|
||||||
|
const theme = getTheme();
|
||||||
|
|
||||||
const iconClass = mergeStyles({
|
const iconClass = mergeStyles({
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
marginRight: "8px",
|
marginRight: "8px",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const classNames = mergeStyleSets({
|
||||||
|
[CopyJobStatusType.Pending]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
||||||
|
[CopyJobStatusType.InProgress]: [{ color: theme.palette.themePrimary }, iconClass],
|
||||||
|
[CopyJobStatusType.Running]: [{ color: theme.palette.themePrimary }, iconClass],
|
||||||
|
[CopyJobStatusType.Partitioning]: [{ color: theme.palette.themePrimary }, iconClass],
|
||||||
|
[CopyJobStatusType.Paused]: [{ color: theme.palette.themePrimary }, iconClass],
|
||||||
|
[CopyJobStatusType.Skipped]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
||||||
|
[CopyJobStatusType.Cancelled]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
||||||
|
[CopyJobStatusType.Failed]: [{ color: theme.semanticColors.errorIcon }, iconClass],
|
||||||
|
[CopyJobStatusType.Faulted]: [{ color: theme.semanticColors.errorIcon }, iconClass],
|
||||||
|
[CopyJobStatusType.Completed]: [{ color: theme.semanticColors.successIcon }, iconClass],
|
||||||
|
unknown: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
||||||
|
});
|
||||||
|
|
||||||
const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
||||||
[CopyJobStatusType.Pending]: "Clock",
|
[CopyJobStatusType.Pending]: "Clock",
|
||||||
[CopyJobStatusType.Paused]: "CirclePause",
|
[CopyJobStatusType.Paused]: "CirclePause",
|
||||||
@@ -19,17 +35,6 @@ const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
|||||||
[CopyJobStatusType.Completed]: "CompletedSolid",
|
[CopyJobStatusType.Completed]: "CompletedSolid",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Icon colors for different statuses
|
|
||||||
const statusIconColors: Partial<Record<CopyJobStatusType, string>> = {
|
|
||||||
[CopyJobStatusType.Failed]: "var(--colorPaletteRedForeground1)",
|
|
||||||
[CopyJobStatusType.Faulted]: "var(--colorPaletteRedForeground1)",
|
|
||||||
[CopyJobStatusType.Completed]: "var(--colorSuccessGreen)",
|
|
||||||
[CopyJobStatusType.InProgress]: "var(--colorBrandForeground1)",
|
|
||||||
[CopyJobStatusType.Running]: "var(--colorBrandForeground1)",
|
|
||||||
[CopyJobStatusType.Partitioning]: "var(--colorBrandForeground1)",
|
|
||||||
[CopyJobStatusType.Paused]: "var(--colorBrandForeground1)",
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CopyJobStatusWithIconProps {
|
export interface CopyJobStatusWithIconProps {
|
||||||
status: CopyJobStatusType;
|
status: CopyJobStatusType;
|
||||||
}
|
}
|
||||||
@@ -42,17 +47,19 @@ const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo((
|
|||||||
CopyJobStatusType.InProgress,
|
CopyJobStatusType.InProgress,
|
||||||
CopyJobStatusType.Partitioning,
|
CopyJobStatusType.Partitioning,
|
||||||
].includes(status);
|
].includes(status);
|
||||||
const iconColor = statusIconColors[status] || "var(--colorNeutralForeground2)";
|
|
||||||
const iconStyle = mergeStyles(iconClass, { color: iconColor });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
{isSpinnerStatus ? (
|
{isSpinnerStatus ? (
|
||||||
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
|
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
|
||||||
) : (
|
) : (
|
||||||
<FontIcon aria-label={status} iconName={iconMap[status] || "UnknownSolid"} className={iconStyle} />
|
<FontIcon
|
||||||
|
aria-label={status}
|
||||||
|
iconName={iconMap[status] || "UnknownSolid"}
|
||||||
|
className={classNames[status] || classNames.unknown}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Text className="themeText">{statusText}</Text>
|
<Text>{statusText}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,17 +11,9 @@ jest.mock("../../Actions/CopyJobActions", () => ({
|
|||||||
|
|
||||||
jest.mock("./CopyJobColumns", () => ({
|
jest.mock("./CopyJobColumns", () => ({
|
||||||
getColumns: jest.fn(() => [
|
getColumns: jest.fn(() => [
|
||||||
{
|
|
||||||
key: "LastUpdatedTime",
|
|
||||||
name: "Date & time",
|
|
||||||
fieldName: "LastUpdatedTime",
|
|
||||||
minWidth: 140,
|
|
||||||
maxWidth: 300,
|
|
||||||
isResizable: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "Name",
|
key: "Name",
|
||||||
name: "Job name",
|
name: "Name",
|
||||||
fieldName: "Name",
|
fieldName: "Name",
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
maxWidth: 300,
|
maxWidth: 300,
|
||||||
@@ -173,165 +165,6 @@ describe("CopyJobsList", () => {
|
|||||||
expect(screen.getByTestId("action-menu-job-2")).toBeInTheDocument();
|
expect(screen.getByTestId("action-menu-job-2")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("action-menu-job-3")).toBeInTheDocument();
|
expect(screen.getByTestId("action-menu-job-3")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders filter TextField with data-test attribute", () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const filterTextField = document.querySelector('[data-test="CopyJobsList/FilterTextField"]');
|
|
||||||
expect(filterTextField).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders search TextField with correct placeholder", () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const searchInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
expect(searchInput).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Filtering", () => {
|
|
||||||
it("filters jobs by Name when text is entered", async () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "Job 1" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters jobs case-insensitively", async () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "test job 1" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows all jobs when filter text is empty", async () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "Job 1" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.change(filterInput, { target: { value: "" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters jobs by Status across all columns", async () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: CopyJobStatusType.Running } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters jobs by Mode across all columns", async () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "Offline" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows no results when filter matches no jobs", async () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "NonExistentJob" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters by partial text match", async () => {
|
|
||||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "Test" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets pagination when filter changes", async () => {
|
|
||||||
const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({
|
|
||||||
...mockJobs[0],
|
|
||||||
ID: `job-${i + 1}`,
|
|
||||||
Name: `Test Job ${i + 1}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
|
||||||
|
|
||||||
// Navigate to page 2
|
|
||||||
fireEvent.click(screen.getByLabelText("Go to next page"));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText("Showing 11 - 20 of 25 items")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply filter - should reset to page 1
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "Job 1" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// Filtered results show from the beginning
|
|
||||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates filtered count in pager", async () => {
|
|
||||||
const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({
|
|
||||||
...mockJobs[0],
|
|
||||||
ID: `job-${i + 1}`,
|
|
||||||
Name: i < 5 ? `Alpha Job ${i + 1}` : `Beta Job ${i + 1}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
|
||||||
|
|
||||||
expect(screen.getByText("Showing 1 - 10 of 25 items")).toBeInTheDocument();
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "Alpha" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByText("Showing 1 - 10 of 25 items")).not.toBeInTheDocument();
|
|
||||||
// Pager should not be visible since filtered results (5) are less than page size (10)
|
|
||||||
expect(screen.queryByLabelText("Go to next page")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Pagination", () => {
|
describe("Pagination", () => {
|
||||||
@@ -509,7 +342,7 @@ describe("CopyJobsList", () => {
|
|||||||
|
|
||||||
describe("Component Props", () => {
|
describe("Component Props", () => {
|
||||||
it("uses default page size when not provided", () => {
|
it("uses default page size when not provided", () => {
|
||||||
const manyJobs: CopyJobType[] = Array.from({ length: 20 }, (_, i) => ({
|
const manyJobs: CopyJobType[] = Array.from({ length: 12 }, (_, i) => ({
|
||||||
...mockJobs[0],
|
...mockJobs[0],
|
||||||
ID: `job-${i + 1}`,
|
ID: `job-${i + 1}`,
|
||||||
Name: `Test Job ${i + 1}`,
|
Name: `Test Job ${i + 1}`,
|
||||||
@@ -518,7 +351,7 @@ describe("CopyJobsList", () => {
|
|||||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />);
|
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />);
|
||||||
|
|
||||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Showing 1 - 15 of 20 items")).toBeInTheDocument();
|
expect(screen.getByText("Showing 1 - 10 of 12 items")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes correct props to getColumns function", async () => {
|
it("passes correct props to getColumns function", async () => {
|
||||||
@@ -607,33 +440,7 @@ describe("CopyJobsList", () => {
|
|||||||
render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />);
|
render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
|
|
||||||
expect(screen.getByText("Showing 1 - 15 of 1000 items")).toBeInTheDocument();
|
expect(screen.getByText("Showing 1 - 10 of 1000 items")).toBeInTheDocument();
|
||||||
});
|
|
||||||
|
|
||||||
it("handles filtering with null or undefined values gracefully", async () => {
|
|
||||||
const jobsWithNullValues: CopyJobType[] = [
|
|
||||||
{
|
|
||||||
...mockJobs[0],
|
|
||||||
ID: "job-with-values",
|
|
||||||
Name: "Valid Job",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...mockJobs[1],
|
|
||||||
ID: "job-null-name",
|
|
||||||
Name: undefined as unknown as string,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
render(<CopyJobsList jobs={jobsWithNullValues} handleActionClick={mockHandleActionClick} />);
|
|
||||||
}).not.toThrow();
|
|
||||||
|
|
||||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
|
||||||
fireEvent.change(filterInput, { target: { value: "Valid" } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText("Valid Job")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,12 +12,9 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Sticky,
|
Sticky,
|
||||||
StickyPositionType,
|
StickyPositionType,
|
||||||
TextField,
|
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import React, { useEffect, useMemo } from "react";
|
import React, { useEffect } from "react";
|
||||||
import Pager from "../../../../Common/Pager";
|
import Pager from "../../../../Common/Pager";
|
||||||
import { useThemeStore } from "../../../../hooks/useTheme";
|
|
||||||
import { getThemeTokens } from "../../../Theme/ThemeUtil";
|
|
||||||
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
|
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
|
||||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||||
import { getColumns } from "./CopyJobColumns";
|
import { getColumns } from "./CopyJobColumns";
|
||||||
@@ -29,42 +26,17 @@ interface CopyJobsListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
container: { height: "100%" } as React.CSSProperties,
|
container: { height: "calc(100vh - 25em)" } as React.CSSProperties,
|
||||||
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
||||||
filterContainer: {
|
|
||||||
margin: "15px 5px",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 15;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
// Columns to search across
|
|
||||||
const searchableFields = ["Name", "Status", "LastUpdatedTime", "Mode"];
|
|
||||||
|
|
||||||
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
|
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
|
||||||
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
|
||||||
const themeTokens = getThemeTokens(isDarkMode);
|
|
||||||
const [startIndex, setStartIndex] = React.useState(0);
|
const [startIndex, setStartIndex] = React.useState(0);
|
||||||
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
|
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
|
||||||
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
||||||
const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false);
|
const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false);
|
||||||
const [filterText, setFilterText] = React.useState<string>("");
|
|
||||||
|
|
||||||
const filteredJobs = useMemo(() => {
|
|
||||||
if (!filterText) {
|
|
||||||
return sortedJobs;
|
|
||||||
}
|
|
||||||
const lowerFilterText = filterText.toLowerCase();
|
|
||||||
return sortedJobs.filter((job: any) => {
|
|
||||||
return searchableFields.some((field) => {
|
|
||||||
const value = job[field];
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return String(value).toLowerCase().includes(lowerFilterText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [sortedJobs, filterText]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSortedJobs(jobs);
|
setSortedJobs(jobs);
|
||||||
@@ -88,15 +60,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
|||||||
setStartIndex(0);
|
setStartIndex(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortableColumns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
|
const columns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
|
||||||
|
|
||||||
const handleFilterTextChange = (
|
|
||||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
||||||
newValue?: string,
|
|
||||||
) => {
|
|
||||||
setFilterText(newValue || "");
|
|
||||||
setStartIndex(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _handleRowClick = (job: CopyJobType) => {
|
const _handleRowClick = (job: CopyJobType) => {
|
||||||
openCopyJobDetailsPanel(job);
|
openCopyJobDetailsPanel(job);
|
||||||
@@ -113,59 +77,30 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
|||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div style={styles.container}>
|
||||||
<Stack verticalFill={true}>
|
<Stack verticalFill={true}>
|
||||||
<Stack.Item>
|
|
||||||
<div style={styles.filterContainer}>
|
|
||||||
<TextField
|
|
||||||
data-test="CopyJobsList/FilterTextField"
|
|
||||||
placeholder="Search jobs..."
|
|
||||||
ariaLabel="Search jobs"
|
|
||||||
value={filterText}
|
|
||||||
onChange={handleFilterTextChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
|
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
|
||||||
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
||||||
<ShimmeredDetailsList
|
<ShimmeredDetailsList
|
||||||
className="CopyJobListContainer"
|
|
||||||
onRenderRow={_onRenderRow}
|
onRenderRow={_onRenderRow}
|
||||||
checkboxVisibility={2}
|
checkboxVisibility={2}
|
||||||
columns={sortableColumns}
|
columns={columns}
|
||||||
items={filteredJobs.slice(startIndex, startIndex + pageSize)}
|
items={sortedJobs.slice(startIndex, startIndex + pageSize)}
|
||||||
enableShimmer={false}
|
enableShimmer={false}
|
||||||
constrainMode={ConstrainMode.unconstrained}
|
constrainMode={ConstrainMode.unconstrained}
|
||||||
layoutMode={DetailsListLayoutMode.justified}
|
layoutMode={DetailsListLayoutMode.justified}
|
||||||
onRenderDetailsHeader={(props, defaultRender) => {
|
onRenderDetailsHeader={(props, defaultRender) => (
|
||||||
const bgColor = themeTokens.colorNeutralBackground3;
|
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
|
||||||
const textColor = themeTokens.colorNeutralForeground1;
|
{defaultRender({ ...props })}
|
||||||
return (
|
</Sticky>
|
||||||
<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>
|
</ScrollablePane>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
{filteredJobs.length > pageSize && (
|
{sortedJobs.length > pageSize && (
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<Pager
|
<Pager
|
||||||
disabled={false}
|
disabled={false}
|
||||||
startIndex={startIndex}
|
startIndex={startIndex}
|
||||||
totalCount={filteredJobs.length}
|
totalCount={sortedJobs.length}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onLoadPage={(startIdx /* pageSize */) => {
|
onLoadPage={(startIdx /* pageSize */) => {
|
||||||
setStartIndex(startIdx);
|
setStartIndex(startIdx);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spin
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -33,7 +33,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with sp
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -53,7 +53,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -66,7 +66,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Cancelled"
|
aria-label="Cancelled"
|
||||||
class="ms-Icon root-105 css-118 mocked-styles"
|
class="ms-Icon root-105 css-118 mocked-style-Cancelled"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -74,7 +74,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Cancelled
|
Cancelled
|
||||||
</span>
|
</span>
|
||||||
@@ -87,7 +87,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Completed"
|
aria-label="Completed"
|
||||||
class="ms-Icon root-105 css-120 mocked-styles"
|
class="ms-Icon root-105 css-120 mocked-style-Completed"
|
||||||
data-icon-name="CompletedSolid"
|
data-icon-name="CompletedSolid"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-5";"
|
style="font-family: "FabricMDL2Icons-5";"
|
||||||
@@ -95,7 +95,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Completed
|
Completed
|
||||||
</span>
|
</span>
|
||||||
@@ -108,7 +108,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Failed"
|
aria-label="Failed"
|
||||||
class="ms-Icon root-105 css-118 mocked-styles"
|
class="ms-Icon root-105 css-118 mocked-style-Failed"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -116,7 +116,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Failed
|
Failed
|
||||||
</span>
|
</span>
|
||||||
@@ -129,7 +129,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Faulted"
|
aria-label="Faulted"
|
||||||
class="ms-Icon root-105 css-118 mocked-styles"
|
class="ms-Icon root-105 css-118 mocked-style-Faulted"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -137,7 +137,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Failed
|
Failed
|
||||||
</span>
|
</span>
|
||||||
@@ -150,7 +150,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Paused"
|
aria-label="Paused"
|
||||||
class="ms-Icon root-105 css-114 mocked-styles"
|
class="ms-Icon root-105 css-114 mocked-style-Paused"
|
||||||
data-icon-name="CirclePause"
|
data-icon-name="CirclePause"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-11";"
|
style="font-family: "FabricMDL2Icons-11";"
|
||||||
@@ -158,7 +158,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Paused
|
Paused
|
||||||
</span>
|
</span>
|
||||||
@@ -171,7 +171,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Pending"
|
aria-label="Pending"
|
||||||
class="ms-Icon root-105 css-111 mocked-styles"
|
class="ms-Icon root-105 css-111 mocked-style-Pending"
|
||||||
data-icon-name="Clock"
|
data-icon-name="Clock"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-2";"
|
style="font-family: "FabricMDL2Icons-2";"
|
||||||
@@ -179,7 +179,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Queued
|
Queued
|
||||||
</span>
|
</span>
|
||||||
@@ -192,7 +192,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Skipped"
|
aria-label="Skipped"
|
||||||
class="ms-Icon root-105 css-116 mocked-styles"
|
class="ms-Icon root-105 css-116 mocked-style-Skipped"
|
||||||
data-icon-name="StatusCircleBlock2"
|
data-icon-name="StatusCircleBlock2"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-9";"
|
style="font-family: "FabricMDL2Icons-9";"
|
||||||
@@ -200,7 +200,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="themeText css-112"
|
class="css-112"
|
||||||
>
|
>
|
||||||
Cancelled
|
Cancelled
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
|
|||||||
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
||||||
import CopyJobsList from "./Components/CopyJobsList";
|
import CopyJobsList from "./Components/CopyJobsList";
|
||||||
|
|
||||||
const FETCH_INTERVAL = 2 * 60 * 1000;
|
const FETCH_INTERVAL_MS = 30 * 1000;
|
||||||
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
||||||
|
|
||||||
interface MonitorCopyJobsProps {
|
interface MonitorCopyJobsProps {
|
||||||
@@ -57,7 +57,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchJobs();
|
fetchJobs();
|
||||||
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL);
|
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS);
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [fetchJobs]);
|
}, [fetchJobs]);
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export interface DatabaseContainerSectionProps {
|
|||||||
containerDisabled?: boolean;
|
containerDisabled?: boolean;
|
||||||
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
|
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
|
||||||
handleOnDemandCreateContainer?: () => void;
|
handleOnDemandCreateContainer?: () => void;
|
||||||
sectionType: "source" | "target";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopyJobContextState {
|
export interface CopyJobContextState {
|
||||||
|
|||||||
@@ -1,52 +1,6 @@
|
|||||||
@import "../../../less/Common/Constants.less";
|
@import "../../../less/Common/Constants.less";
|
||||||
|
|
||||||
.themedTextFieldStyles() {
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common theme-aware classes
|
|
||||||
.themeText {
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeTextSecondary {
|
|
||||||
color: var(--colorNeutralForeground2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeLinkText {
|
|
||||||
color: var(--colorBrandForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeBackground {
|
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeBackgroundSecondary {
|
|
||||||
background-color: var(--colorNeutralBackground2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#containerCopyWrapper {
|
#containerCopyWrapper {
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
|
|
||||||
.centerContent {
|
.centerContent {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -55,30 +9,20 @@
|
|||||||
.noCopyJobsMessage {
|
.noCopyJobsMessage {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
color: var(--colorNeutralForeground2);
|
color: @FocusColor;
|
||||||
}
|
}
|
||||||
button.createCopyJobButton {
|
button.createCopyJobButton {
|
||||||
color: var(--colorBrandForeground1);
|
color: @LinkColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.createCopyJobScreensContainer {
|
.createCopyJobScreensContainer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1em 1.5em;
|
padding: 1em 1.5em;
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
|
|
||||||
.pointInTimeRestoreContainer, .onlineCopyContainer {
|
.pointInTimeRestoreContainer, .onlineCopyContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-label {
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordionHeaderText {
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -127,7 +71,7 @@
|
|||||||
}
|
}
|
||||||
.foreground {
|
.foreground {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: var(--colorNeutralBackground2);
|
background-color: #f9f9f9;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
transform: translate(0%, -9%);
|
transform: translate(0%, -9%);
|
||||||
@@ -136,31 +80,14 @@
|
|||||||
.createCopyJobErrorMessageBar {
|
.createCopyJobErrorMessageBar {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
body.isDarkMode & {
|
|
||||||
.ms-TooltipHost .ms-Image {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themedTextFieldStyles();
|
|
||||||
|
|
||||||
.migrationTypeDescription {
|
|
||||||
p {
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--colorBrandForeground1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.create-container-link-btn {
|
.create-container-link-btn {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
color: var(--colorBrandForeground1);
|
color: @LinkColor;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create collection panel */
|
/* Create collection panel */
|
||||||
@@ -179,10 +106,6 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
body.isDarkMode & {
|
|
||||||
.themedTextFieldStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-DetailsList {
|
.ms-DetailsList {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -191,36 +114,33 @@
|
|||||||
padding: @DefaultSpace 20px;
|
padding: @DefaultSpace 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: var(--colorNeutralForeground1);
|
color: @BaseHigh;
|
||||||
background-color: var(--colorNeutralBackground2);
|
background-color: @BaseLow;
|
||||||
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--colorNeutralBackground3);
|
background-color: @BaseMediumLow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ms-DetailsHeader-cellTitle {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ms-DetailsRow {
|
.ms-DetailsRow {
|
||||||
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--colorNeutralBackground2);
|
background-color: @BaseMediumLow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ms-DetailsRow-cell {
|
.ms-DetailsRow-cell {
|
||||||
padding: @MediumSpace 20px;
|
padding: @MediumSpace 20px;
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: var(--colorNeutralForeground1);
|
color: @BaseHigh;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.jobNameLink {
|
.jobNameLink {
|
||||||
color: var(--colorBrandForeground1);
|
color: @LinkColor;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -248,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
.ms-DetailsRow-cell {
|
.ms-DetailsRow-cell {
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: var(--colorNeutralForeground1);
|
color: @BaseHigh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
AddGlobalSecondaryIndexPanelProps,
|
AddGlobalSecondaryIndexPanelProps,
|
||||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric, isFabricNative, openRestoreContainerDialog } from "Platform/Fabric/FabricUtil";
|
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -35,7 +35,6 @@ import StoredProcedure from "./Tree/StoredProcedure";
|
|||||||
import Trigger from "./Tree/Trigger";
|
import Trigger from "./Tree/Trigger";
|
||||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||||
import { useSelectedNode } from "./useSelectedNode";
|
import { useSelectedNode } from "./useSelectedNode";
|
||||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
|
||||||
|
|
||||||
export interface CollectionContextMenuButtonParams {
|
export interface CollectionContextMenuButtonParams {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
@@ -61,17 +60,6 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isFabricNative() && !userContext.fabricContext?.isReadOnly) {
|
|
||||||
const features = extractFeatures();
|
|
||||||
if (features?.enableRestoreContainer) {
|
|
||||||
items.push({
|
|
||||||
iconSrc: AddCollectionIcon,
|
|
||||||
onClick: () => openRestoreContainerDialog(),
|
|
||||||
label: `Restore ${getCollectionName()}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { IndexingPolicy } from "@azure/cosmos";
|
|
||||||
import { act } from "@testing-library/react";
|
|
||||||
import { AuthType } from "AuthType";
|
import { AuthType } from "AuthType";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||||
@@ -447,49 +444,3 @@ describe("SettingsComponent", () => {
|
|||||||
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toBe(true);
|
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("SettingsComponent - indexing policy subscription", () => {
|
|
||||||
const baseProps: SettingsComponentProps = {
|
|
||||||
settingsTab: new CollectionSettingsTabV2({
|
|
||||||
collection: collection,
|
|
||||||
tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
node: undefined,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
it("subscribes to the correct container's indexing policy and updates state on change", async () => {
|
|
||||||
const containerId = collection.id();
|
|
||||||
const mockIndexingPolicy: IndexingPolicy = {
|
|
||||||
automatic: false,
|
|
||||||
indexingMode: "lazy",
|
|
||||||
includedPaths: [{ path: "/foo/*" }],
|
|
||||||
excludedPaths: [{ path: "/bar/*" }],
|
|
||||||
compositeIndexes: [],
|
|
||||||
spatialIndexes: [],
|
|
||||||
vectorIndexes: [],
|
|
||||||
fullTextIndexes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
|
||||||
const instance = wrapper.instance() as SettingsComponent;
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
useIndexingPolicyStore.setState({
|
|
||||||
indexingPolicies: {
|
|
||||||
[containerId]: mockIndexingPolicy,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Wait for the async refreshCollectionData to complete
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.state("indexingPolicyContent")).toEqual(mockIndexingPolicy);
|
|
||||||
expect(wrapper.state("indexingPolicyContentBaseline")).toEqual(mockIndexingPolicy);
|
|
||||||
// @ts-expect-error: rawDataModel is intentionally accessed for test validation
|
|
||||||
expect(instance.collection.rawDataModel.indexingPolicy).toEqual(mockIndexingPolicy);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
ThroughputBucketsComponent,
|
ThroughputBucketsComponent,
|
||||||
ThroughputBucketsComponentProps,
|
ThroughputBucketsComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
@@ -74,6 +73,7 @@ import {
|
|||||||
parseConflictResolutionMode,
|
parseConflictResolutionMode,
|
||||||
parseConflictResolutionProcedure,
|
parseConflictResolutionProcedure,
|
||||||
} from "./SettingsUtils";
|
} from "./SettingsUtils";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
content: JSX.Element;
|
content: JSX.Element;
|
||||||
@@ -182,7 +182,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
private throughputBucketsEnabled: boolean;
|
private throughputBucketsEnabled: boolean;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
private unsubscribe: () => void;
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -312,13 +312,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
if (this.isCollectionSettingsTab) {
|
if (this.isCollectionSettingsTab) {
|
||||||
this.refreshIndexTransformationProgress();
|
this.refreshIndexTransformationProgress();
|
||||||
this.loadMongoIndexes();
|
this.loadMongoIndexes();
|
||||||
this.unsubscribe = useIndexingPolicyStore.subscribe(
|
|
||||||
() => {
|
|
||||||
this.refreshCollectionData();
|
|
||||||
},
|
|
||||||
(state) => state.indexingPolicies[this.collection?.id()],
|
|
||||||
);
|
|
||||||
this.refreshCollectionData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setBaseline();
|
this.setBaseline();
|
||||||
@@ -326,11 +319,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentWillUnmount(): void {
|
|
||||||
if (this.unsubscribe) {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentDidUpdate(): void {
|
componentDidUpdate(): void {
|
||||||
if (this.props.settingsTab.isActive()) {
|
if (this.props.settingsTab.isActive()) {
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
@@ -860,6 +849,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
{ name: "name_of_property", query: "query_to_compute_property" },
|
{ name: "name_of_property", query: "query_to_compute_property" },
|
||||||
] as DataModels.ComputedProperties;
|
] as DataModels.ComputedProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const throughputBuckets = this.offer?.throughputBuckets;
|
const throughputBuckets = this.offer?.throughputBuckets;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1019,31 +1009,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
startKey,
|
startKey,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
private refreshCollectionData = async (): Promise<void> => {
|
|
||||||
const containerId = this.collection.id();
|
|
||||||
const latestIndexingPolicy = useIndexingPolicyStore.getState().indexingPolicies[containerId];
|
|
||||||
const rawPolicy = latestIndexingPolicy ?? this.collection.indexingPolicy();
|
|
||||||
|
|
||||||
const latestCollection: DataModels.IndexingPolicy = {
|
|
||||||
automatic: rawPolicy?.automatic ?? true,
|
|
||||||
indexingMode: rawPolicy?.indexingMode ?? "consistent",
|
|
||||||
includedPaths: rawPolicy?.includedPaths ?? [],
|
|
||||||
excludedPaths: rawPolicy?.excludedPaths ?? [],
|
|
||||||
compositeIndexes: rawPolicy?.compositeIndexes ?? [],
|
|
||||||
spatialIndexes: rawPolicy?.spatialIndexes ?? [],
|
|
||||||
vectorIndexes: rawPolicy?.vectorIndexes ?? [],
|
|
||||||
fullTextIndexes: rawPolicy?.fullTextIndexes ?? [],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.collection.rawDataModel.indexingPolicy = latestCollection;
|
|
||||||
this.setState({
|
|
||||||
indexingPolicyContent: latestCollection,
|
|
||||||
indexingPolicyContentBaseline: latestCollection,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
||||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
this.state.isContainerPolicyDirty ||
|
this.state.isContainerPolicyDirty ||
|
||||||
@@ -1283,6 +1252,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||||
throughputError: this.state.throughputError,
|
throughputError: this.state.throughputError,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.isCollectionSettingsTab) {
|
if (!this.isCollectionSettingsTab) {
|
||||||
return (
|
return (
|
||||||
<div className="settingsV2MainContainer">
|
<div className="settingsV2MainContainer">
|
||||||
|
|||||||
@@ -155,12 +155,7 @@ export class ComputedPropertiesComponent extends React.Component<
|
|||||||
</Link>
|
</Link>
|
||||||
  about how to define computed properties and how to use them.
|
  about how to define computed properties and how to use them.
|
||||||
</Text>
|
</Text>
|
||||||
<div
|
<div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
|
||||||
className="settingsV2Editor"
|
|
||||||
tabIndex={0}
|
|
||||||
ref={this.computedPropertiesDiv}
|
|
||||||
data-test="computed-properties-editor"
|
|
||||||
></div>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { MessageBar, MessageBarType } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
import { MessageBar, MessageBarType } from "@fluentui/react";
|
||||||
import {
|
import {
|
||||||
mongoIndexTransformationRefreshingMessage,
|
mongoIndexTransformationRefreshingMessage,
|
||||||
renderMongoIndexTransformationRefreshMessage,
|
renderMongoIndexTransformationRefreshMessage,
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
|
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||||
import { isIndexTransforming } from "../../SettingsUtils";
|
import { isIndexTransforming } from "../../SettingsUtils";
|
||||||
|
|
||||||
export interface IndexingPolicyRefreshComponentProps {
|
export interface IndexingPolicyRefreshComponentProps {
|
||||||
|
|||||||
@@ -302,8 +302,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
);
|
);
|
||||||
|
|
||||||
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
|
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||||
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
|
{ key: GeospatialConfigType.Geography, text: "Geography" },
|
||||||
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
|
{ key: GeospatialConfigType.Geometry, text: "Geometry" },
|
||||||
];
|
];
|
||||||
|
|
||||||
private getGeoSpatialComponent = (): JSX.Element => (
|
private getGeoSpatialComponent = (): JSX.Element => (
|
||||||
|
|||||||
@@ -103,10 +103,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
|||||||
offText="Inactive"
|
offText="Inactive"
|
||||||
checked={bucket.maxThroughputPercentage !== 100}
|
checked={bucket.maxThroughputPercentage !== 100}
|
||||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||||
styles={{
|
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
||||||
root: { marginBottom: 0 },
|
|
||||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
|
||||||
}}
|
|
||||||
></Toggle>
|
></Toggle>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ exports[`ComputedPropertiesComponent renders 1`] = `
|
|||||||
</Text>
|
</Text>
|
||||||
<div
|
<div
|
||||||
className="settingsV2Editor"
|
className="settingsV2Editor"
|
||||||
data-test="computed-properties-editor"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -167,12 +167,10 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ariaLabel": "geography-option",
|
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ariaLabel": "geometry-option",
|
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
@@ -654,12 +652,10 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ariaLabel": "geography-option",
|
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ariaLabel": "geometry-option",
|
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
@@ -1228,12 +1224,10 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ariaLabel": "geography-option",
|
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ariaLabel": "geometry-option",
|
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
@@ -1766,12 +1760,10 @@ exports[`SubSettingsComponent renders 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ariaLabel": "geography-option",
|
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ariaLabel": "geometry-option",
|
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
@@ -2338,12 +2330,10 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ariaLabel": "geography-option",
|
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ariaLabel": "geometry-option",
|
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -153,16 +153,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
"rawDataModel": {
|
||||||
"indexingPolicy": {
|
|
||||||
"automatic": true,
|
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
|
||||||
"indexingMode": "consistent",
|
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
},
|
|
||||||
"uniqueKeyPolicy": {
|
"uniqueKeyPolicy": {
|
||||||
"uniqueKeys": [
|
"uniqueKeys": [
|
||||||
{
|
{
|
||||||
@@ -274,16 +264,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
"rawDataModel": {
|
||||||
"indexingPolicy": {
|
|
||||||
"automatic": true,
|
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
|
||||||
"indexingMode": "consistent",
|
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
},
|
|
||||||
"uniqueKeyPolicy": {
|
"uniqueKeyPolicy": {
|
||||||
"uniqueKeys": [
|
"uniqueKeys": [
|
||||||
{
|
{
|
||||||
@@ -496,16 +476,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
"rawDataModel": {
|
||||||
"indexingPolicy": {
|
|
||||||
"automatic": true,
|
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
|
||||||
"indexingMode": "consistent",
|
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
},
|
|
||||||
"uniqueKeyPolicy": {
|
"uniqueKeyPolicy": {
|
||||||
"uniqueKeys": [
|
"uniqueKeys": [
|
||||||
{
|
{
|
||||||
@@ -683,16 +653,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
"rawDataModel": {
|
||||||
"indexingPolicy": {
|
|
||||||
"automatic": true,
|
|
||||||
"compositeIndexes": [],
|
|
||||||
"excludedPaths": [],
|
|
||||||
"fullTextIndexes": [],
|
|
||||||
"includedPaths": [],
|
|
||||||
"indexingMode": "consistent",
|
|
||||||
"spatialIndexes": [],
|
|
||||||
"vectorIndexes": [],
|
|
||||||
},
|
|
||||||
"uniqueKeyPolicy": {
|
"uniqueKeyPolicy": {
|
||||||
"uniqueKeys": [
|
"uniqueKeys": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexTyp
|
|||||||
const labelStyles = {
|
const labelStyles = {
|
||||||
root: {
|
root: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,8 +63,6 @@ const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldSt
|
|||||||
field: {
|
field: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
padding: "0 8px",
|
padding: "0 8px",
|
||||||
backgroundColor: "var(--colorNeutralBackground1)",
|
|
||||||
color: "var(--colorNeutralForeground1)",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -437,14 +437,13 @@ export default class Explorer {
|
|||||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||||
if (isFabricMirroredKey()) {
|
if (isFabricMirroredKey()) {
|
||||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
||||||
} else {
|
return;
|
||||||
await (userContext.authType === AuthType.ResourceToken
|
|
||||||
? this.refreshDatabaseForResourceToken()
|
|
||||||
: this.refreshAllDatabases());
|
|
||||||
await this.refreshNotebookList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logConsoleInfo("Successfully refreshed databases");
|
await (userContext.authType === AuthType.ResourceToken
|
||||||
|
? this.refreshDatabaseForResourceToken()
|
||||||
|
: this.refreshAllDatabases());
|
||||||
|
await this.refreshNotebookList();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Facade
|
// Facade
|
||||||
|
|||||||
@@ -853,7 +853,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{!isSynapseLinkEnabled() && (
|
{!isSynapseLinkEnabled() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
<Text variant="small">
|
||||||
Azure Synapse Link is required for creating an analytical store{" "}
|
Azure Synapse Link is required for creating an analytical store{" "}
|
||||||
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -475,11 +475,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={
|
|
||||||
{
|
|
||||||
"color": "var(--colorNeutralForeground1)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
Azure Synapse Link is required for creating an analytical store
|
Azure Synapse Link is required for creating an analytical store
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
|
||||||
export const PanelLoadingScreen: React.FunctionComponent = () => (
|
export const PanelLoadingScreen: React.FunctionComponent = () => (
|
||||||
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerLoaderforcopyJobs">
|
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
|
||||||
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
||||||
</div>
|
</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."
|
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
|
||||||
/>
|
/>
|
||||||
{uploadFileData?.length > 0 && (
|
{uploadFileData?.length > 0 && (
|
||||||
<div className="fileUploadSummaryContainer" data-test="file-upload-status">
|
<div className="fileUploadSummaryContainer">
|
||||||
<b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b>
|
<b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b>
|
||||||
<DetailsList
|
<DetailsList
|
||||||
items={uploadFileData}
|
items={uploadFileData}
|
||||||
|
|||||||
@@ -54,6 +54,6 @@
|
|||||||
.mainButtonsContainer {
|
.mainButtonsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0 16px;
|
gap: 0 16px;
|
||||||
margin: 40px auto
|
margin-bottom: 10px
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,23 +164,6 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
const container = explorer;
|
const container = explorer;
|
||||||
const subscriptions: Array<{ dispose: () => void }> = [];
|
const subscriptions: Array<{ dispose: () => void }> = [];
|
||||||
|
|
||||||
let title: string;
|
|
||||||
let subtitle: string;
|
|
||||||
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "Postgres":
|
|
||||||
title = "Welcome to Azure Cosmos DB for PostgreSQL";
|
|
||||||
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
|
||||||
break;
|
|
||||||
case "VCoreMongo":
|
|
||||||
title = "Welcome to Azure DocumentDB (with MongoDB compatibility)";
|
|
||||||
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
title = "Welcome to Azure Cosmos DB";
|
|
||||||
subtitle = "Globally distributed, multi-model database service for any scale";
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
{
|
{
|
||||||
@@ -919,11 +902,10 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.splashScreenContainer}>
|
<div className={styles.splashScreenContainer}>
|
||||||
<div className={styles.splashScreen}>
|
<div className={styles.splashScreen}>
|
||||||
<h2 className={styles.title} role="heading" aria-label={title}>
|
<h2 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
|
||||||
{title}
|
Welcome to Azure Cosmos DB<span className="activePatch"></span>
|
||||||
<span className="activePatch"></span>
|
|
||||||
</h2>
|
</h2>
|
||||||
<div className={styles.subtitle}>{subtitle}</div>
|
<div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div>
|
||||||
{getSplashScreenButtons()}
|
{getSplashScreenButtons()}
|
||||||
{useCarousel.getState().showCoachMark && (
|
{useCarousel.getState().showCoachMark && (
|
||||||
<Coachmark
|
<Coachmark
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import { CircleFilled } from "@fluentui/react-icons";
|
|
||||||
import type { IIndexMetric } from "Explorer/Tabs/QueryTab/ResultsView";
|
|
||||||
import { useIndexAdvisorStyles } from "Explorer/Tabs/QueryTab/StylesAdvisor";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
// SDK response format
|
|
||||||
export interface IndexMetricsResponse {
|
|
||||||
UtilizedIndexes?: {
|
|
||||||
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
|
|
||||||
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
|
|
||||||
};
|
|
||||||
PotentialIndexes?: {
|
|
||||||
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
|
|
||||||
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseIndexMetrics(indexMetrics: IndexMetricsResponse): {
|
|
||||||
included: IIndexMetric[];
|
|
||||||
notIncluded: IIndexMetric[];
|
|
||||||
} {
|
|
||||||
const included: IIndexMetric[] = [];
|
|
||||||
const notIncluded: IIndexMetric[] = [];
|
|
||||||
|
|
||||||
// Process UtilizedIndexes (Included in Current Policy)
|
|
||||||
if (indexMetrics.UtilizedIndexes) {
|
|
||||||
// Single indexes
|
|
||||||
indexMetrics.UtilizedIndexes.SingleIndexes?.forEach((index) => {
|
|
||||||
included.push({
|
|
||||||
index: index.IndexSpec,
|
|
||||||
impact: index.IndexImpactScore || "Utilized",
|
|
||||||
section: "Included",
|
|
||||||
path: index.IndexSpec,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Composite indexes
|
|
||||||
indexMetrics.UtilizedIndexes.CompositeIndexes?.forEach((index) => {
|
|
||||||
const compositeSpec = index.IndexSpecs.join(", ");
|
|
||||||
included.push({
|
|
||||||
index: compositeSpec,
|
|
||||||
impact: index.IndexImpactScore || "Utilized",
|
|
||||||
section: "Included",
|
|
||||||
composite: index.IndexSpecs.map((spec) => {
|
|
||||||
const [path, order] = spec.trim().split(/\s+/);
|
|
||||||
return {
|
|
||||||
path: path.trim(),
|
|
||||||
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process PotentialIndexes (Not Included in Current Policy)
|
|
||||||
if (indexMetrics.PotentialIndexes) {
|
|
||||||
// Single indexes
|
|
||||||
indexMetrics.PotentialIndexes.SingleIndexes?.forEach((index) => {
|
|
||||||
notIncluded.push({
|
|
||||||
index: index.IndexSpec,
|
|
||||||
impact: index.IndexImpactScore || "Unknown",
|
|
||||||
section: "Not Included",
|
|
||||||
path: index.IndexSpec,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Composite indexes
|
|
||||||
indexMetrics.PotentialIndexes.CompositeIndexes?.forEach((index) => {
|
|
||||||
const compositeSpec = index.IndexSpecs.join(", ");
|
|
||||||
notIncluded.push({
|
|
||||||
index: compositeSpec,
|
|
||||||
impact: index.IndexImpactScore || "Unknown",
|
|
||||||
section: "Not Included",
|
|
||||||
composite: index.IndexSpecs.map((spec) => {
|
|
||||||
const [path, order] = spec.trim().split(/\s+/);
|
|
||||||
return {
|
|
||||||
path: path.trim(),
|
|
||||||
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { included, notIncluded };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderImpactDots = (impact: string): JSX.Element => {
|
|
||||||
const style = useIndexAdvisorStyles();
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (impact === "High") {
|
|
||||||
count = 3;
|
|
||||||
} else if (impact === "Medium") {
|
|
||||||
count = 2;
|
|
||||||
} else if (impact === "Low") {
|
|
||||||
count = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.indexAdvisorImpactDots}>
|
|
||||||
{Array.from({ length: count }).map((_, i) => (
|
|
||||||
<CircleFilled key={i} className={style.indexAdvisorImpactDot} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -3,21 +3,18 @@ import QueryError from "Common/QueryError";
|
|||||||
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
|
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
|
||||||
import { MessageBanner } from "Explorer/Controls/MessageBanner";
|
import { MessageBanner } from "Explorer/Controls/MessageBanner";
|
||||||
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||||
import useZoomLevel from "hooks/useZoomLevel";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { conditionalClass } from "Utils/StyleUtils";
|
|
||||||
import RunQuery from "../../../../images/RunQuery.png";
|
import RunQuery from "../../../../images/RunQuery.png";
|
||||||
import { QueryResults } from "../../../Contracts/ViewModels";
|
import { QueryResults } from "../../../Contracts/ViewModels";
|
||||||
import { ErrorList } from "./ErrorList";
|
import { ErrorList } from "./ErrorList";
|
||||||
import { ResultsView } from "./ResultsView";
|
import { ResultsView } from "./ResultsView";
|
||||||
|
import useZoomLevel from "hooks/useZoomLevel";
|
||||||
|
import { conditionalClass } from "Utils/StyleUtils";
|
||||||
|
|
||||||
export interface ResultsViewProps {
|
export interface ResultsViewProps {
|
||||||
isMongoDB: boolean;
|
isMongoDB: boolean;
|
||||||
queryResults: QueryResults;
|
queryResults: QueryResults;
|
||||||
executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>;
|
executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>;
|
||||||
queryEditorContent?: string;
|
|
||||||
databaseId?: string;
|
|
||||||
containerId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryResultProps extends ResultsViewProps {
|
interface QueryResultProps extends ResultsViewProps {
|
||||||
@@ -52,8 +49,6 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
|
|||||||
queryResults,
|
queryResults,
|
||||||
executeQueryDocumentsPage,
|
executeQueryDocumentsPage,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
databaseId,
|
|
||||||
containerId,
|
|
||||||
}: QueryResultProps): JSX.Element => {
|
}: QueryResultProps): JSX.Element => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
|
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
|
||||||
@@ -96,9 +91,6 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
|
|||||||
queryResults={queryResults}
|
queryResults={queryResults}
|
||||||
executeQueryDocumentsPage={executeQueryDocumentsPage}
|
executeQueryDocumentsPage={executeQueryDocumentsPage}
|
||||||
isMongoDB={isMongoDB}
|
isMongoDB={isMongoDB}
|
||||||
queryEditorContent={queryEditorContent}
|
|
||||||
databaseId={databaseId}
|
|
||||||
containerId={containerId}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ExecuteQueryCallToAction />
|
<ExecuteQueryCallToAction />
|
||||||
|
|||||||
@@ -52,9 +52,8 @@ describe("QueryTabComponent", () => {
|
|||||||
copilotVersion: "v3.0",
|
copilotVersion: "v3.0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const propsMock: Readonly<IQueryTabComponentProps> = {
|
const propsMock: Readonly<IQueryTabComponentProps> = {
|
||||||
collection: { databaseId: "CopilotSampleDB", id: () => "CopilotContainer" },
|
collection: { databaseId: "CopilotSampleDB" },
|
||||||
onTabAccessor: () => jest.fn(),
|
onTabAccessor: () => jest.fn(),
|
||||||
isExecutionError: false,
|
isExecutionError: false,
|
||||||
tabId: "mockTabId",
|
tabId: "mockTabId",
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { useMonacoTheme } from "hooks/useTheme";
|
|||||||
import React, { Fragment, createRef } from "react";
|
import React, { Fragment, createRef } from "react";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
import create from "zustand";
|
|
||||||
//TODO: Uncomment next two lines when query copilot is reinstated in DE
|
//TODO: Uncomment next two lines when query copilot is reinstated in DE
|
||||||
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||||
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||||
@@ -58,20 +57,6 @@ import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
|||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
import "./QueryTabComponent.less";
|
import "./QueryTabComponent.less";
|
||||||
|
|
||||||
export interface QueryMetadataStore {
|
|
||||||
userQuery: string;
|
|
||||||
databaseId: string;
|
|
||||||
containerId: string;
|
|
||||||
setMetadata: (query1: string, db: string, container: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
|
|
||||||
userQuery: "",
|
|
||||||
databaseId: "",
|
|
||||||
containerId: "",
|
|
||||||
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
enum ToggleState {
|
enum ToggleState {
|
||||||
Result,
|
Result,
|
||||||
QueryMetrics,
|
QueryMetrics,
|
||||||
@@ -279,10 +264,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onExecuteQueryClick = async (): Promise<void> => {
|
public onExecuteQueryClick = async (): Promise<void> => {
|
||||||
const query1 = this.state.sqlQueryEditorContent;
|
|
||||||
const db = this.props.collection.databaseId;
|
|
||||||
const container = this.props.collection.id();
|
|
||||||
useQueryMetadataStore.getState().setMetadata(query1, db, container);
|
|
||||||
this._iterator = undefined;
|
this._iterator = undefined;
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -799,8 +780,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
errors={this.props.copilotStore?.errors}
|
errors={this.props.copilotStore?.errors}
|
||||||
isExecuting={this.props.copilotStore?.isExecuting}
|
isExecuting={this.props.copilotStore?.isExecuting}
|
||||||
queryResults={this.props.copilotStore?.queryResults}
|
queryResults={this.props.copilotStore?.queryResults}
|
||||||
databaseId={this.props.collection.databaseId}
|
|
||||||
containerId={this.props.collection.id()}
|
|
||||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||||
QueryDocumentsPerPage(
|
QueryDocumentsPerPage(
|
||||||
firstItemIndex,
|
firstItemIndex,
|
||||||
@@ -816,8 +795,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
errors={this.state.errors}
|
errors={this.state.errors}
|
||||||
isExecuting={this.state.isExecuting}
|
isExecuting={this.state.isExecuting}
|
||||||
queryResults={this.state.queryResults}
|
queryResults={this.state.queryResults}
|
||||||
databaseId={this.props.collection.databaseId}
|
|
||||||
containerId={this.props.collection.id()}
|
|
||||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||||
this._executeQueryDocumentsPage(firstItemIndex)
|
this._executeQueryDocumentsPage(firstItemIndex)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
|
||||||
import { IndexAdvisorTab } from "Explorer/Tabs/QueryTab/ResultsView";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const mockReplace = jest.fn();
|
|
||||||
const mockFetchAll = jest.fn();
|
|
||||||
const mockRead = jest.fn();
|
|
||||||
const mockLogConsoleProgress = jest.fn();
|
|
||||||
const mockHandleError = jest.fn();
|
|
||||||
|
|
||||||
const indexMetricsResponse = {
|
|
||||||
UtilizedIndexes: {
|
|
||||||
SingleIndexes: [{ IndexSpec: "/foo/?", IndexImpactScore: "High" }],
|
|
||||||
CompositeIndexes: [{ IndexSpecs: ["/baz/? DESC", "/qux/? ASC"], IndexImpactScore: "Low" }],
|
|
||||||
},
|
|
||||||
PotentialIndexes: {
|
|
||||||
SingleIndexes: [{ IndexSpec: "/bar/?", IndexImpactScore: "Medium" }],
|
|
||||||
CompositeIndexes: [] as Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockQueryResults = {
|
|
||||||
documents: [] as unknown[],
|
|
||||||
hasMoreResults: false,
|
|
||||||
itemCount: 0,
|
|
||||||
firstItemIndex: 0,
|
|
||||||
lastItemIndex: 0,
|
|
||||||
requestCharge: 0,
|
|
||||||
activityId: "test-activity-id",
|
|
||||||
};
|
|
||||||
|
|
||||||
mockRead.mockResolvedValue({
|
|
||||||
resource: {
|
|
||||||
indexingPolicy: {
|
|
||||||
automatic: true,
|
|
||||||
indexingMode: "consistent",
|
|
||||||
includedPaths: [{ path: "/*" }, { path: "/foo/?" }],
|
|
||||||
excludedPaths: [],
|
|
||||||
},
|
|
||||||
partitionKey: "pk",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReplace.mockResolvedValue({
|
|
||||||
resource: {
|
|
||||||
indexingPolicy: {
|
|
||||||
automatic: true,
|
|
||||||
indexingMode: "consistent",
|
|
||||||
includedPaths: [{ path: "/*" }],
|
|
||||||
excludedPaths: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock("Common/CosmosClient", () => ({
|
|
||||||
client: () => ({
|
|
||||||
database: () => ({
|
|
||||||
container: () => ({
|
|
||||||
items: {
|
|
||||||
query: () => ({
|
|
||||||
fetchAll: mockFetchAll,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
read: mockRead,
|
|
||||||
replace: mockReplace,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("./StylesAdvisor", () => ({
|
|
||||||
useIndexAdvisorStyles: () => ({}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../../Utils/NotificationConsoleUtils", () => ({
|
|
||||||
logConsoleProgress: (...args: unknown[]) => {
|
|
||||||
mockLogConsoleProgress(...args);
|
|
||||||
return () => {};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../../Common/ErrorHandlingUtils", () => ({
|
|
||||||
handleError: (...args: unknown[]) => mockHandleError(...args),
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
mockFetchAll.mockResolvedValue({ indexMetrics: indexMetricsResponse });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("IndexAdvisorTab Basic Tests", () => {
|
|
||||||
test("component renders without crashing", () => {
|
|
||||||
const { container } = render(
|
|
||||||
<IndexAdvisorTab queryEditorContent="SELECT * FROM c" databaseId="db1" containerId="col1" />,
|
|
||||||
);
|
|
||||||
expect(container).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders component and handles missing parameters", () => {
|
|
||||||
const { container } = render(<IndexAdvisorTab />);
|
|
||||||
expect(container).toBeTruthy();
|
|
||||||
// Should not crash when parameters are missing
|
|
||||||
});
|
|
||||||
|
|
||||||
test("fetches index metrics with query results", async () => {
|
|
||||||
render(
|
|
||||||
<IndexAdvisorTab
|
|
||||||
queryResults={mockQueryResults}
|
|
||||||
queryEditorContent="SELECT * FROM c"
|
|
||||||
databaseId="db1"
|
|
||||||
containerId="col1"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("displays content after loading", async () => {
|
|
||||||
render(
|
|
||||||
<IndexAdvisorTab
|
|
||||||
queryResults={mockQueryResults}
|
|
||||||
queryEditorContent="SELECT * FROM c"
|
|
||||||
databaseId="db1"
|
|
||||||
containerId="col1"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
// Wait for the component to finish loading
|
|
||||||
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
|
|
||||||
// Component should have rendered some content
|
|
||||||
expect(screen.getByText(/Index Advisor/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls log console progress when fetching metrics", async () => {
|
|
||||||
render(
|
|
||||||
<IndexAdvisorTab
|
|
||||||
queryResults={mockQueryResults}
|
|
||||||
queryEditorContent="SELECT * FROM c"
|
|
||||||
databaseId="db1"
|
|
||||||
containerId="col1"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(mockLogConsoleProgress).toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("handles error when fetch fails", async () => {
|
|
||||||
mockFetchAll.mockRejectedValueOnce(new Error("fetch failed"));
|
|
||||||
render(
|
|
||||||
<IndexAdvisorTab
|
|
||||||
queryResults={mockQueryResults}
|
|
||||||
queryEditorContent="SELECT * FROM c"
|
|
||||||
databaseId="db1"
|
|
||||||
containerId="col1"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(mockHandleError).toHaveBeenCalled(), { timeout: 3000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders with all required props", () => {
|
|
||||||
const { container } = render(
|
|
||||||
<IndexAdvisorTab
|
|
||||||
queryResults={mockQueryResults}
|
|
||||||
queryEditorContent="SELECT * FROM c"
|
|
||||||
databaseId="testDb"
|
|
||||||
containerId="testContainer"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(container).toBeTruthy();
|
|
||||||
expect(container.firstChild).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { CompositePath, IndexingPolicy } from "@azure/cosmos";
|
|
||||||
import { FontIcon } from "@fluentui/react";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
DataGrid,
|
DataGrid,
|
||||||
DataGridBody,
|
DataGridBody,
|
||||||
DataGridCell,
|
DataGridCell,
|
||||||
@@ -11,45 +8,28 @@ import {
|
|||||||
DataGridRow,
|
DataGridRow,
|
||||||
SelectTabData,
|
SelectTabData,
|
||||||
SelectTabEvent,
|
SelectTabEvent,
|
||||||
Spinner,
|
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableColumnDefinition,
|
TableColumnDefinition,
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
createTableColumn,
|
createTableColumn,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { ArrowDownloadRegular, ChevronDown20Regular, ChevronRight20Regular, CopyRegular } from "@fluentui/react-icons";
|
import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons";
|
||||||
import copy from "clipboard-copy";
|
|
||||||
import { HttpHeaders } from "Common/Constants";
|
import { HttpHeaders } from "Common/Constants";
|
||||||
import MongoUtility from "Common/MongoUtility";
|
import MongoUtility from "Common/MongoUtility";
|
||||||
import { QueryMetrics } from "Contracts/DataModels";
|
import { QueryMetrics } from "Contracts/DataModels";
|
||||||
import { QueryResults } from "Contracts/ViewModels";
|
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import {
|
|
||||||
parseIndexMetrics,
|
|
||||||
renderImpactDots,
|
|
||||||
type IndexMetricsResponse,
|
|
||||||
} from "Explorer/Tabs/QueryTab/IndexAdvisorUtils";
|
|
||||||
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import copy from "clipboard-copy";
|
||||||
import create from "zustand";
|
import React, { useCallback, useState } from "react";
|
||||||
import { client } from "../../../Common/CosmosClient";
|
|
||||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
|
||||||
import { sampleDataClient } from "../../../Common/SampleDataClient";
|
|
||||||
import { ResultsViewProps } from "./QueryResultSection";
|
import { ResultsViewProps } from "./QueryResultSection";
|
||||||
import { useIndexAdvisorStyles } from "./StylesAdvisor";
|
|
||||||
enum ResultsTabs {
|
enum ResultsTabs {
|
||||||
Results = "results",
|
Results = "results",
|
||||||
QueryStats = "queryStats",
|
QueryStats = "queryStats",
|
||||||
IndexAdvisor = "indexadv",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
|
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
@@ -543,331 +523,14 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IIndexMetric {
|
export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
|
||||||
index: string;
|
|
||||||
impact: string;
|
|
||||||
section: "Included" | "Not Included" | "Header";
|
|
||||||
path?: string;
|
|
||||||
composite?: { path: string; order: string }[];
|
|
||||||
}
|
|
||||||
export const IndexAdvisorTab: React.FC<{
|
|
||||||
queryResults?: QueryResults;
|
|
||||||
queryEditorContent?: string;
|
|
||||||
databaseId?: string;
|
|
||||||
containerId?: string;
|
|
||||||
}> = ({ queryResults, queryEditorContent, databaseId, containerId }) => {
|
|
||||||
const style = useIndexAdvisorStyles();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [indexMetrics, setIndexMetrics] = useState<IndexMetricsResponse | null>(null);
|
|
||||||
const [showIncluded, setShowIncluded] = useState(true);
|
|
||||||
const [showNotIncluded, setShowNotIncluded] = useState(true);
|
|
||||||
const [selectedIndexes, setSelectedIndexes] = useState<IIndexMetric[]>([]);
|
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
|
||||||
const [updateMessageShown, setUpdateMessageShown] = useState(false);
|
|
||||||
const [included, setIncludedIndexes] = useState<IIndexMetric[]>([]);
|
|
||||||
const [notIncluded, setNotIncludedIndexes] = useState<IIndexMetric[]>([]);
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
|
||||||
const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
|
|
||||||
const indexingMetricsDocLink = "https://learn.microsoft.com/azure/cosmos-db/nosql/index-metrics";
|
|
||||||
|
|
||||||
const fetchIndexMetrics = async () => {
|
|
||||||
if (!queryEditorContent || !databaseId || !containerId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
|
|
||||||
try {
|
|
||||||
const querySpec = {
|
|
||||||
query: queryEditorContent,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use sampleDataClient for CopilotSampleDB, regular client for other databases
|
|
||||||
const cosmosClient = databaseId === "CopilotSampleDB" ? sampleDataClient() : client();
|
|
||||||
|
|
||||||
const sdkResponse = await cosmosClient
|
|
||||||
.database(databaseId)
|
|
||||||
.container(containerId)
|
|
||||||
.items.query(querySpec, {
|
|
||||||
populateIndexMetrics: true,
|
|
||||||
})
|
|
||||||
.fetchAll();
|
|
||||||
|
|
||||||
const parsedMetrics =
|
|
||||||
typeof sdkResponse.indexMetrics === "string" ? JSON.parse(sdkResponse.indexMetrics) : sdkResponse.indexMetrics;
|
|
||||||
|
|
||||||
setIndexMetrics(parsedMetrics);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`);
|
|
||||||
} finally {
|
|
||||||
clearMessage();
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch index metrics when query results change (i.e., when Execute Query is clicked)
|
|
||||||
useEffect(() => {
|
|
||||||
if (queryEditorContent && databaseId && containerId && queryResults) {
|
|
||||||
fetchIndexMetrics();
|
|
||||||
}
|
|
||||||
}, [queryResults]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!indexMetrics) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { included, notIncluded } = parseIndexMetrics(indexMetrics);
|
|
||||||
setIncludedIndexes(included);
|
|
||||||
setNotIncludedIndexes(notIncluded);
|
|
||||||
if (justUpdatedPolicy) {
|
|
||||||
setJustUpdatedPolicy(false);
|
|
||||||
} else {
|
|
||||||
setUpdateMessageShown(false);
|
|
||||||
}
|
|
||||||
}, [indexMetrics]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const allSelected =
|
|
||||||
notIncluded.length > 0 && notIncluded.every((item) => selectedIndexes.some((s) => s.index === item.index));
|
|
||||||
setSelectAll(allSelected);
|
|
||||||
}, [selectedIndexes, notIncluded]);
|
|
||||||
|
|
||||||
const handleCheckboxChange = (indexObj: IIndexMetric, checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedIndexes((prev) => [...prev, indexObj]);
|
|
||||||
} else {
|
|
||||||
setSelectedIndexes((prev) => prev.filter((item) => item.index !== indexObj.index));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAll = (checked: boolean) => {
|
|
||||||
setSelectAll(checked);
|
|
||||||
setSelectedIndexes(checked ? notIncluded : []);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdatePolicy = async () => {
|
|
||||||
setIsUpdating(true);
|
|
||||||
try {
|
|
||||||
const containerRef = client().database(databaseId).container(containerId);
|
|
||||||
const { resource: containerDef } = await containerRef.read();
|
|
||||||
|
|
||||||
const newIncludedPaths = selectedIndexes
|
|
||||||
.filter((index) => !index.composite)
|
|
||||||
.map((index) => {
|
|
||||||
return {
|
|
||||||
path: index.path,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const newCompositeIndexes: CompositePath[][] = selectedIndexes
|
|
||||||
.filter((index) => Array.isArray(index.composite))
|
|
||||||
.map(
|
|
||||||
(index) =>
|
|
||||||
(index.composite as { path: string; order: string }[]).map((comp) => ({
|
|
||||||
path: comp.path,
|
|
||||||
order: comp.order === "descending" ? "descending" : "ascending",
|
|
||||||
})) as CompositePath[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedPolicy: IndexingPolicy = {
|
|
||||||
...containerDef.indexingPolicy,
|
|
||||||
includedPaths: [...(containerDef.indexingPolicy?.includedPaths || []), ...newIncludedPaths],
|
|
||||||
compositeIndexes: [...(containerDef.indexingPolicy?.compositeIndexes || []), ...newCompositeIndexes],
|
|
||||||
automatic: containerDef.indexingPolicy?.automatic ?? true,
|
|
||||||
indexingMode: containerDef.indexingPolicy?.indexingMode ?? "consistent",
|
|
||||||
excludedPaths: containerDef.indexingPolicy?.excludedPaths ?? [],
|
|
||||||
};
|
|
||||||
await containerRef.replace({
|
|
||||||
id: containerId,
|
|
||||||
partitionKey: containerDef.partitionKey,
|
|
||||||
indexingPolicy: updatedPolicy,
|
|
||||||
});
|
|
||||||
useIndexingPolicyStore.getState().setIndexingPolicyFor(containerId, updatedPolicy);
|
|
||||||
const selectedIndexSet = new Set(selectedIndexes.map((s) => s.index));
|
|
||||||
const updatedNotIncluded: typeof notIncluded = [];
|
|
||||||
const newlyIncluded: typeof included = [];
|
|
||||||
for (const item of notIncluded) {
|
|
||||||
if (selectedIndexSet.has(item.index)) {
|
|
||||||
newlyIncluded.push(item);
|
|
||||||
} else {
|
|
||||||
updatedNotIncluded.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newIncluded = [...included, ...newlyIncluded];
|
|
||||||
const newNotIncluded = updatedNotIncluded;
|
|
||||||
setIncludedIndexes(newIncluded);
|
|
||||||
setNotIncludedIndexes(newNotIncluded);
|
|
||||||
setSelectedIndexes([]);
|
|
||||||
setSelectAll(false);
|
|
||||||
setUpdateMessageShown(true);
|
|
||||||
setJustUpdatedPolicy(true);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to update indexing policy:", err);
|
|
||||||
} finally {
|
|
||||||
setIsUpdating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderRow = (item: IIndexMetric, index: number) => {
|
|
||||||
const isHeader = item.section === "Header";
|
|
||||||
const isNotIncluded = item.section === "Not Included";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell colSpan={2}>
|
|
||||||
<div className={style.indexAdvisorGrid}>
|
|
||||||
{isNotIncluded ? (
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedIndexes.some((selected) => selected.index === item.index)}
|
|
||||||
onChange={(_, data) => handleCheckboxChange(item, data.checked === true)}
|
|
||||||
/>
|
|
||||||
) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? (
|
|
||||||
<Checkbox checked={selectAll} onChange={(_, data) => handleSelectAll(data.checked === true)} />
|
|
||||||
) : (
|
|
||||||
<div className={style.indexAdvisorCheckboxSpacer}></div>
|
|
||||||
)}
|
|
||||||
{isHeader ? (
|
|
||||||
<span
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => {
|
|
||||||
if (item.index === "Included in Current Policy") {
|
|
||||||
setShowIncluded(!showIncluded);
|
|
||||||
} else if (item.index === "Not Included in Current Policy") {
|
|
||||||
setShowNotIncluded(!showNotIncluded);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.index === "Included in Current Policy" ? (
|
|
||||||
showIncluded ? (
|
|
||||||
<ChevronDown20Regular />
|
|
||||||
) : (
|
|
||||||
<ChevronRight20Regular />
|
|
||||||
)
|
|
||||||
) : showNotIncluded ? (
|
|
||||||
<ChevronDown20Regular />
|
|
||||||
) : (
|
|
||||||
<ChevronRight20Regular />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<div className={style.indexAdvisorChevronSpacer}></div>
|
|
||||||
)}
|
|
||||||
<div className={isHeader ? style.indexAdvisorRowBold : style.indexAdvisorRowNormal}>{item.index}</div>
|
|
||||||
<div className={isHeader ? style.indexAdvisorRowImpactHeader : style.indexAdvisorRowImpact}>
|
|
||||||
{!isHeader && item.impact}
|
|
||||||
</div>
|
|
||||||
<div>{!isHeader && renderImpactDots(item.impact)}</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const indexMetricItems = React.useMemo(() => {
|
|
||||||
const items: IIndexMetric[] = [];
|
|
||||||
items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" });
|
|
||||||
if (showNotIncluded) {
|
|
||||||
notIncluded.forEach((item) => items.push({ ...item, section: "Not Included" }));
|
|
||||||
}
|
|
||||||
items.push({ index: "Included in Current Policy", impact: "", section: "Header" });
|
|
||||||
if (showIncluded) {
|
|
||||||
included.forEach((item) => items.push({ ...item, section: "Included" }));
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}, [included, notIncluded, showIncluded, showNotIncluded]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Spinner
|
|
||||||
size="small"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--spinner-size": "16px",
|
|
||||||
"--spinner-thickness": "2px",
|
|
||||||
"--spinner-color": "#0078D4",
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={style.indexAdvisorMessage}>
|
|
||||||
{updateMessageShown ? (
|
|
||||||
<>
|
|
||||||
<span className={style.indexAdvisorSuccessIcon}>
|
|
||||||
<FontIcon iconName="CheckMark" style={{ color: "white", fontSize: 12 }} />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Your indexing policy has been updated with the new included paths. You may review the changes in Scale &
|
|
||||||
Settings.
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
Index Advisor uses Indexing Metrics to suggest query paths that, when included in your indexing policy,
|
|
||||||
can improve the performance of this query by reducing RU costs and lowering latency.{" "}
|
|
||||||
<a href={indexingMetricsDocLink} target="_blank" rel="noopener noreferrer">
|
|
||||||
Learn more about Indexing Metrics
|
|
||||||
</a>
|
|
||||||
.{" "}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={style.indexAdvisorTitle}>Indexes analysis</div>
|
|
||||||
<Table className={style.indexAdvisorTable}>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={2}>
|
|
||||||
<div className={style.indexAdvisorGrid}>
|
|
||||||
<div className={style.indexAdvisorCheckboxSpacer}></div>
|
|
||||||
<div className={style.indexAdvisorChevronSpacer}></div>
|
|
||||||
<div>Index</div>
|
|
||||||
<div>
|
|
||||||
<span style={{ whiteSpace: "nowrap" }}>Estimated Impact</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>{indexMetricItems.map(renderRow)}</TableBody>
|
|
||||||
</Table>
|
|
||||||
{selectedIndexes.length > 0 && (
|
|
||||||
<div className={style.indexAdvisorButtonBar}>
|
|
||||||
{isUpdating ? (
|
|
||||||
<div className={style.indexAdvisorButtonSpinner}>
|
|
||||||
<Spinner size="tiny" />{" "}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button onClick={handleUpdatePolicy} className={style.indexAdvisorButton}>
|
|
||||||
Update Indexing Policy with selected index(es)
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const ResultsView: React.FC<ResultsViewProps> = ({
|
|
||||||
isMongoDB,
|
|
||||||
queryResults,
|
|
||||||
executeQueryDocumentsPage,
|
|
||||||
queryEditorContent,
|
|
||||||
databaseId,
|
|
||||||
containerId,
|
|
||||||
}) => {
|
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
|
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
|
||||||
|
|
||||||
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
|
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
|
||||||
setActiveTab(data.value as ResultsTabs);
|
setActiveTab(data.value as ResultsTabs);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
|
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
|
||||||
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
|
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
|
||||||
@@ -885,13 +548,6 @@ export const ResultsView: React.FC<ResultsViewProps> = ({
|
|||||||
>
|
>
|
||||||
Query Stats
|
Query Stats
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
|
||||||
data-test="QueryTab/ResultsPane/ResultsView/IndexAdvisorTab"
|
|
||||||
id={ResultsTabs.IndexAdvisor}
|
|
||||||
value={ResultsTabs.IndexAdvisor}
|
|
||||||
>
|
|
||||||
Index Advisor
|
|
||||||
</Tab>
|
|
||||||
</TabList>
|
</TabList>
|
||||||
<div className={styles.queryResultsTabContentContainer}>
|
<div className={styles.queryResultsTabContentContainer}>
|
||||||
{activeTab === ResultsTabs.Results && (
|
{activeTab === ResultsTabs.Results && (
|
||||||
@@ -902,30 +558,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
|
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
|
||||||
{activeTab === ResultsTabs.IndexAdvisor && (
|
|
||||||
<IndexAdvisorTab
|
|
||||||
queryResults={queryResults}
|
|
||||||
queryEditorContent={queryEditorContent}
|
|
||||||
databaseId={databaseId}
|
|
||||||
containerId={containerId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export interface IndexingPolicyStore {
|
|
||||||
indexingPolicies: { [containerId: string]: IndexingPolicy };
|
|
||||||
setIndexingPolicyFor: (containerId: string, indexingPolicy: IndexingPolicy) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useIndexingPolicyStore = create<IndexingPolicyStore>((set) => ({
|
|
||||||
indexingPolicies: {},
|
|
||||||
setIndexingPolicyFor: (containerId, indexingPolicy) =>
|
|
||||||
set((state) => ({
|
|
||||||
indexingPolicies: {
|
|
||||||
...state.indexingPolicies,
|
|
||||||
[containerId]: { ...indexingPolicy },
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import { makeStyles } from "@fluentui/react-components";
|
|
||||||
export type IndexAdvisorStyles = ReturnType<typeof useIndexAdvisorStyles>;
|
|
||||||
export const useIndexAdvisorStyles = makeStyles({
|
|
||||||
indexAdvisorMessage: {
|
|
||||||
padding: "1rem",
|
|
||||||
fontSize: "1.2rem",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
},
|
|
||||||
indexAdvisorSuccessIcon: {
|
|
||||||
width: "18px",
|
|
||||||
height: "18px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: "#107C10",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
indexAdvisorTitle: {
|
|
||||||
padding: "1rem",
|
|
||||||
fontSize: "1.3rem",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
indexAdvisorTable: {
|
|
||||||
display: "block",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "7rem",
|
|
||||||
},
|
|
||||||
indexAdvisorGrid: {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "30px 30px 1fr 50px 120px",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "15px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
indexAdvisorCheckboxSpacer: {
|
|
||||||
width: "18px",
|
|
||||||
height: "18px",
|
|
||||||
},
|
|
||||||
indexAdvisorChevronSpacer: {
|
|
||||||
width: "24px",
|
|
||||||
},
|
|
||||||
indexAdvisorRowBold: {
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
indexAdvisorRowNormal: {
|
|
||||||
fontWeight: "normal",
|
|
||||||
},
|
|
||||||
indexAdvisorRowImpactHeader: {
|
|
||||||
fontSize: 0,
|
|
||||||
},
|
|
||||||
indexAdvisorRowImpact: {
|
|
||||||
fontWeight: "normal",
|
|
||||||
},
|
|
||||||
indexAdvisorImpactDot: {
|
|
||||||
color: "#0078D4",
|
|
||||||
fontSize: "12px",
|
|
||||||
display: "inline-flex",
|
|
||||||
},
|
|
||||||
indexAdvisorImpactDots: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "4px",
|
|
||||||
},
|
|
||||||
indexAdvisorButtonBar: {
|
|
||||||
padding: "1rem",
|
|
||||||
marginTop: "-7rem",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
},
|
|
||||||
indexAdvisorButtonSpinner: {
|
|
||||||
marginTop: "1rem",
|
|
||||||
minWidth: "320px",
|
|
||||||
minHeight: "40px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "left",
|
|
||||||
justifyContent: "left",
|
|
||||||
marginLeft: "10rem",
|
|
||||||
},
|
|
||||||
indexAdvisorButton: {
|
|
||||||
backgroundColor: "#0078D4",
|
|
||||||
color: "white",
|
|
||||||
padding: "8px 16px",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
marginTop: "1rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
transition: "background 0.2s",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "#005a9e",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import create from "zustand";
|
|
||||||
|
|
||||||
interface QueryMetadataStore {
|
|
||||||
userQuery: string;
|
|
||||||
databaseId: string;
|
|
||||||
containerId: string;
|
|
||||||
setMetadata: (query1: string, db: string, container: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
|
|
||||||
userQuery: "",
|
|
||||||
databaseId: "",
|
|
||||||
containerId: "",
|
|
||||||
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
|
|
||||||
}));
|
|
||||||
@@ -435,6 +435,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
});
|
});
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return createdResource;
|
return createdResource;
|
||||||
},
|
},
|
||||||
(createError) => {
|
(createError) => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user