mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-25 12:44:06 +00:00
Compare commits
13 Commits
copilot/su
...
users/aisa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae5cb679bf | ||
|
|
4615af0c1c | ||
|
|
07378fc8c3 | ||
|
|
178cbfaf18 | ||
|
|
1db1c9448a | ||
|
|
7b299aac39 | ||
|
|
896b3e974e | ||
|
|
e6461cf079 | ||
|
|
92c8afd166 | ||
|
|
234e4181fc | ||
|
|
38823ac86f | ||
|
|
b71ea50972 | ||
|
|
e27cff0553 |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -192,6 +192,9 @@ 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
|
||||||
@@ -210,6 +213,8 @@ 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
|
||||||
|
|||||||
@@ -406,7 +406,11 @@ body {
|
|||||||
width: 440px;
|
width: 440px;
|
||||||
min-height: 565px;
|
min-height: 565px;
|
||||||
}
|
}
|
||||||
|
.dataExplorerLoaderforcopyJobs{
|
||||||
|
width: 100%;
|
||||||
|
min-height: 565px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
.dataExplorerTabLoaderContainer {
|
.dataExplorerTabLoaderContainer {
|
||||||
left: initial;
|
left: initial;
|
||||||
top: initial;
|
top: initial;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
|
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
|
||||||
|
import { useThemeStore } from "hooks/useTheme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface LoadingOverlayProps {
|
interface LoadingOverlayProps {
|
||||||
@@ -7,6 +8,7 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -16,7 +18,7 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
|
|||||||
data-test="loading-overlay"
|
data-test="loading-overlay"
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "rgba(255,255,255,0.9)",
|
backgroundColor: isDarkMode ? "rgba(32, 31, 30, 0.9)" : "rgba(255,255,255,0.9)",
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -24,7 +26,11 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Spinner size={SpinnerSize.large} label={label} styles={{ label: { fontWeight: 600 } }} />
|
<Spinner
|
||||||
|
size={SpinnerSize.large}
|
||||||
|
label={label}
|
||||||
|
styles={{ label: { fontWeight: 600, color: isDarkMode ? "#ffffff" : "#323130" } }}
|
||||||
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,3 +11,14 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Override dark mode inherit for pagination icons */
|
||||||
|
body.isDarkMode .pager-container .ms-Button .ms-Button-icon,
|
||||||
|
body.isDarkMode .pager-container .ms-Button i {
|
||||||
|
color: var(--colorBrandForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.isDarkMode .pager-container .ms-Button:disabled .ms-Button-icon,
|
||||||
|
body.isDarkMode .pager-container .ms-Button:disabled i {
|
||||||
|
color: var(--colorNeutralForegroundDisabled);
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ const Pager: React.FC<PagerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={className || "pager-container"}>
|
<div className={className || "pager-container"}>
|
||||||
{showItemCount && (
|
{showItemCount && (
|
||||||
<Text>
|
<Text className="themeText">
|
||||||
Showing {startIndex + 1} - {endIndex} of {totalCount} items
|
Showing {startIndex + 1} - {endIndex} of {totalCount} items
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -82,7 +82,7 @@ const Pager: React.FC<PagerProps> = ({
|
|||||||
disabled={disabled || currentPage === 1}
|
disabled={disabled || currentPage === 1}
|
||||||
styles={iconButtonStyles}
|
styles={iconButtonStyles}
|
||||||
/>
|
/>
|
||||||
<Text>
|
<Text className="themeText">
|
||||||
Page {currentPage} of {totalPages}
|
Page {currentPage} of {totalPages}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
SqlStoredProcedureCreateUpdateParameters,
|
SqlStoredProcedureCreateUpdateParameters,
|
||||||
SqlStoredProcedureResource,
|
SqlStoredProcedureResource,
|
||||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ export async function createStoredProcedure(
|
|||||||
): Promise<StoredProcedureDefinition & Resource> {
|
): Promise<StoredProcedureDefinition & Resource> {
|
||||||
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
|
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
|
||||||
try {
|
try {
|
||||||
|
let resource: StoredProcedureDefinition & Resource;
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
@@ -60,14 +61,16 @@ export async function createStoredProcedure(
|
|||||||
storedProcedure.id,
|
storedProcedure.id,
|
||||||
createSprocParams,
|
createSprocParams,
|
||||||
);
|
);
|
||||||
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
resource = rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
||||||
|
} else {
|
||||||
|
const response = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.container(collectionId)
|
||||||
|
.scripts.storedProcedures.create(storedProcedure);
|
||||||
|
resource = response.resource;
|
||||||
}
|
}
|
||||||
|
logConsoleInfo(`Successfully created stored procedure ${storedProcedure.id}`);
|
||||||
const response = await client()
|
return resource;
|
||||||
.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 { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ export async function createTrigger(
|
|||||||
): Promise<TriggerDefinition | SqlTriggerResource> {
|
): Promise<TriggerDefinition | SqlTriggerResource> {
|
||||||
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
|
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
|
||||||
try {
|
try {
|
||||||
|
let resource: SqlTriggerResource | TriggerDefinition;
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
@@ -52,14 +53,16 @@ export async function createTrigger(
|
|||||||
trigger.id,
|
trigger.id,
|
||||||
createTriggerParams,
|
createTriggerParams,
|
||||||
);
|
);
|
||||||
return rpResponse && rpResponse.properties?.resource;
|
resource = rpResponse && rpResponse.properties?.resource;
|
||||||
|
} else {
|
||||||
|
const sdkResponse = await client()
|
||||||
|
.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}`);
|
||||||
const response = await client()
|
return resource;
|
||||||
.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 { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ export async function createUserDefinedFunction(
|
|||||||
): Promise<UserDefinedFunctionDefinition & Resource> {
|
): Promise<UserDefinedFunctionDefinition & Resource> {
|
||||||
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
|
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
|
||||||
try {
|
try {
|
||||||
|
let resource: UserDefinedFunctionDefinition & Resource;
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
@@ -60,14 +61,17 @@ export async function createUserDefinedFunction(
|
|||||||
userDefinedFunction.id,
|
userDefinedFunction.id,
|
||||||
createUDFParams,
|
createUDFParams,
|
||||||
);
|
);
|
||||||
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await client()
|
resource = rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||||
.database(databaseId)
|
} else {
|
||||||
.container(collectionId)
|
const response = await client()
|
||||||
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
.database(databaseId)
|
||||||
return response?.resource;
|
.container(collectionId)
|
||||||
|
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
||||||
|
resource = response.resource;
|
||||||
|
}
|
||||||
|
logConsoleInfo(`Successfully created user defined function ${userDefinedFunction.id}`);
|
||||||
|
return resource;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export async function deleteStoredProcedure(
|
|||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
|
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
|
||||||
}
|
}
|
||||||
|
logConsoleProgress(`Successfully deleted stored procedure ${storedProcedureId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
|
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
|
|||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
|
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
|
||||||
}
|
}
|
||||||
|
logConsoleProgress(`Successfully deleted trigger ${triggerId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
|
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
|
|||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
|
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
|
||||||
}
|
}
|
||||||
|
logConsoleProgress(`Successfully deleted user defined function ${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
|
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ export interface Offer {
|
|||||||
export interface ThroughputBucket {
|
export interface ThroughputBucket {
|
||||||
id: number;
|
id: number;
|
||||||
maxThroughputPercentage: number;
|
maxThroughputPercentage: number;
|
||||||
|
isDefaultBucket?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SDKOfferDefinition extends Resource {
|
export interface SDKOfferDefinition extends Resource {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
||||||
import * as Logger from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||||
@@ -30,6 +31,7 @@ jest.mock("../../../Common/Logger");
|
|||||||
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
|
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
|
||||||
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
|
||||||
jest.mock("../CopyJobUtils");
|
jest.mock("../CopyJobUtils");
|
||||||
|
jest.mock("../../../Common/dataAccess/dataTransfers");
|
||||||
|
|
||||||
describe("CopyJobActions", () => {
|
describe("CopyJobActions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -154,33 +156,31 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should fetch and format copy jobs successfully", async () => {
|
it("should fetch and format copy jobs successfully", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "job-1",
|
||||||
jobName: "job-1",
|
status: "InProgress",
|
||||||
status: "InProgress",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 50,
|
||||||
processedCount: 50,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "online",
|
||||||
mode: "online",
|
duration: "01:30:45",
|
||||||
duration: "01:30:45",
|
source: {
|
||||||
source: {
|
component: "CosmosDBSql",
|
||||||
component: "CosmosDBSql",
|
databaseName: "source-db",
|
||||||
databaseName: "source-db",
|
containerName: "source-container",
|
||||||
containerName: "source-container",
|
},
|
||||||
},
|
destination: {
|
||||||
destination: {
|
component: "CosmosDBSql",
|
||||||
component: "CosmosDBSql",
|
databaseName: "target-db",
|
||||||
databaseName: "target-db",
|
containerName: "target-container",
|
||||||
containerName: "target-container",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -201,38 +201,36 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should filter jobs by CosmosDBSql component", async () => {
|
it("should filter jobs by CosmosDBSql component", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "sql-job",
|
||||||
jobName: "sql-job",
|
status: "Completed",
|
||||||
status: "Completed",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 100,
|
||||||
processedCount: 100,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "offline",
|
||||||
mode: "offline",
|
duration: "02:00:00",
|
||||||
duration: "02:00:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
properties: {
|
{
|
||||||
jobName: "other-job",
|
properties: {
|
||||||
status: "Completed",
|
jobName: "other-job",
|
||||||
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
status: "Completed",
|
||||||
processedCount: 100,
|
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 100,
|
||||||
mode: "offline",
|
totalCount: 100,
|
||||||
duration: "01:00:00",
|
mode: "offline",
|
||||||
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
duration: "01:00:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
|
||||||
},
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -247,38 +245,36 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should sort jobs by last updated time (newest first)", async () => {
|
it("should sort jobs by last updated time (newest first)", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "older-job",
|
||||||
jobName: "older-job",
|
status: "Completed",
|
||||||
status: "Completed",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 100,
|
||||||
processedCount: 100,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "offline",
|
||||||
mode: "offline",
|
duration: "01:00:00",
|
||||||
duration: "01:00:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
properties: {
|
{
|
||||||
jobName: "newer-job",
|
properties: {
|
||||||
status: "InProgress",
|
jobName: "newer-job",
|
||||||
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
status: "InProgress",
|
||||||
processedCount: 50,
|
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
|
||||||
totalCount: 100,
|
processedCount: 50,
|
||||||
mode: "online",
|
totalCount: 100,
|
||||||
duration: "00:30:00",
|
mode: "online",
|
||||||
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
duration: "00:30:00",
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
|
||||||
},
|
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -293,25 +289,23 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should calculate completion percentage correctly", async () => {
|
it("should calculate completion percentage correctly", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "job-1",
|
||||||
jobName: "job-1",
|
status: "InProgress",
|
||||||
status: "InProgress",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 75,
|
||||||
processedCount: 75,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "online",
|
||||||
mode: "online",
|
duration: "01:00:00",
|
||||||
duration: "01:00:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -325,25 +319,23 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle zero total count gracefully", async () => {
|
it("should handle zero total count gracefully", async () => {
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "job-1",
|
||||||
jobName: "job-1",
|
status: "Pending",
|
||||||
status: "Pending",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 0,
|
||||||
processedCount: 0,
|
totalCount: 0,
|
||||||
totalCount: 0,
|
mode: "online",
|
||||||
mode: "online",
|
duration: "00:00:00",
|
||||||
duration: "00:00:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -361,26 +353,24 @@ describe("CopyJobActions", () => {
|
|||||||
message: "Error message line 1\r\n\r\nError message line 2",
|
message: "Error message line 1\r\n\r\nError message line 2",
|
||||||
code: "ErrorCode123",
|
code: "ErrorCode123",
|
||||||
};
|
};
|
||||||
const mockResponse = {
|
const mockResponse = [
|
||||||
value: [
|
{
|
||||||
{
|
properties: {
|
||||||
properties: {
|
jobName: "failed-job",
|
||||||
jobName: "failed-job",
|
status: "Failed",
|
||||||
status: "Failed",
|
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
||||||
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
|
processedCount: 50,
|
||||||
processedCount: 50,
|
totalCount: 100,
|
||||||
totalCount: 100,
|
mode: "offline",
|
||||||
mode: "offline",
|
duration: "00:30:00",
|
||||||
duration: "00:30:00",
|
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
||||||
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
|
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
||||||
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
|
error: mockError,
|
||||||
error: mockError,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
];
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue(mockResponse);
|
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
|
||||||
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
|
||||||
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
formattedDateTime: "1/1/2025, 10:00:00 AM",
|
||||||
timestamp: 1704106800000,
|
timestamp: 1704106800000,
|
||||||
@@ -408,7 +398,7 @@ describe("CopyJobActions", () => {
|
|||||||
};
|
};
|
||||||
(global as any).AbortController = jest.fn(() => mockAbortController);
|
(global as any).AbortController = jest.fn(() => mockAbortController);
|
||||||
|
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({ value: [] });
|
(getDataTransferJobs as jest.Mock).mockResolvedValue([]);
|
||||||
|
|
||||||
getCopyJobs();
|
getCopyJobs();
|
||||||
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
||||||
@@ -418,9 +408,7 @@ describe("CopyJobActions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error for invalid response format", async () => {
|
it("should throw error for invalid response format", async () => {
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockResolvedValue({
|
(getDataTransferJobs as jest.Mock).mockResolvedValue("not-an-array");
|
||||||
value: "not-an-array",
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
|
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
|
||||||
});
|
});
|
||||||
@@ -430,7 +418,7 @@ describe("CopyJobActions", () => {
|
|||||||
message: "Aborted",
|
message: "Aborted",
|
||||||
content: JSON.stringify({ message: "signal is aborted without reason" }),
|
content: JSON.stringify({ message: "signal is aborted without reason" }),
|
||||||
};
|
};
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(abortError);
|
(getDataTransferJobs as jest.Mock).mockRejectedValue(abortError);
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toMatchObject({
|
await expect(getCopyJobs()).rejects.toMatchObject({
|
||||||
message: expect.stringContaining("Previous copy job request was cancelled."),
|
message: expect.stringContaining("Previous copy job request was cancelled."),
|
||||||
@@ -439,7 +427,7 @@ describe("CopyJobActions", () => {
|
|||||||
|
|
||||||
it("should handle generic errors", async () => {
|
it("should handle generic errors", async () => {
|
||||||
const genericError = new Error("Network error");
|
const genericError = new Error("Network error");
|
||||||
(dataTransferService.listByDatabaseAccount as jest.Mock).mockRejectedValue(genericError);
|
(getDataTransferJobs as jest.Mock).mockRejectedValue(genericError);
|
||||||
|
|
||||||
await expect(getCopyJobs()).rejects.toThrow("Network error");
|
await expect(getCopyJobs()).rejects.toThrow("Network error");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
||||||
import { logError } from "../../../Common/Logger";
|
import { logError } from "../../../Common/Logger";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import {
|
import {
|
||||||
cancel,
|
cancel,
|
||||||
complete,
|
complete,
|
||||||
create,
|
create,
|
||||||
listByDatabaseAccount,
|
|
||||||
pause,
|
pause,
|
||||||
resume,
|
resume,
|
||||||
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
||||||
@@ -63,14 +63,8 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
|
|||||||
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
|
||||||
userContext.databaseAccount?.id || "",
|
userContext.databaseAccount?.id || "",
|
||||||
);
|
);
|
||||||
const response = await listByDatabaseAccount(
|
const jobs = await getDataTransferJobs(subscriptionId, resourceGroup, accountName, copyJobsAbortController.signal);
|
||||||
subscriptionId,
|
|
||||||
resourceGroup,
|
|
||||||
accountName,
|
|
||||||
copyJobsAbortController.signal,
|
|
||||||
);
|
|
||||||
|
|
||||||
const jobs = response.value || [];
|
|
||||||
if (!Array.isArray(jobs)) {
|
if (!Array.isArray(jobs)) {
|
||||||
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
throw new Error("Invalid migration job status response: Expected an array of jobs.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe("CopyJobCommandBar", () => {
|
|||||||
|
|
||||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ describe("CopyJobCommandBar", () => {
|
|||||||
|
|
||||||
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
render(<CopyJobCommandBar explorer={mockExplorer} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
|
||||||
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
|
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,11 +175,11 @@ describe("CopyJobCommandBar", () => {
|
|||||||
mockConvertButton.mockReturnValue([]);
|
mockConvertButton.mockReturnValue([]);
|
||||||
|
|
||||||
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
|
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1, false);
|
||||||
|
|
||||||
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
|
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
|
||||||
|
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2, false);
|
||||||
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
|
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
import { useThemeStore } from "../../../hooks/useTheme";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
|
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
|
||||||
|
import { getThemeTokens } from "../../Theme/ThemeUtil";
|
||||||
import { ContainerCopyProps } from "../Types/CopyJobTypes";
|
import { ContainerCopyProps } from "../Types/CopyJobTypes";
|
||||||
import { getCommandBarButtons } from "./Utils";
|
import { getCommandBarButtons } from "./Utils";
|
||||||
|
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
|
||||||
const rootStyle = {
|
|
||||||
root: {
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
|
||||||
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer);
|
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||||
|
const themeTokens = getThemeTokens(isDarkMode);
|
||||||
|
const backgroundColor = themeTokens.colorNeutralBackground1;
|
||||||
|
|
||||||
|
const rootStyle = {
|
||||||
|
root: {
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer, isDarkMode);
|
||||||
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
|
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer">
|
<div className="commandBarContainer" style={{ backgroundColor }}>
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
styles={rootStyle}
|
styles={rootStyle}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe("CommandBar Utils", () => {
|
|||||||
|
|
||||||
describe("getCommandBarButtons", () => {
|
describe("getCommandBarButtons", () => {
|
||||||
it("should return an array of command button props", () => {
|
it("should return an array of command button props", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons).toBeDefined();
|
expect(buttons).toBeDefined();
|
||||||
expect(Array.isArray(buttons)).toBe(true);
|
expect(Array.isArray(buttons)).toBe(true);
|
||||||
@@ -58,7 +58,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include create copy job button", () => {
|
it("should include create copy job button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const createButton = buttons[0];
|
const createButton = buttons[0];
|
||||||
|
|
||||||
expect(createButton).toBeDefined();
|
expect(createButton).toBeDefined();
|
||||||
@@ -70,7 +70,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include refresh button", () => {
|
it("should include refresh button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
expect(refreshButton).toBeDefined();
|
expect(refreshButton).toBeDefined();
|
||||||
@@ -80,11 +80,11 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should include feedback button when platform is Portal", () => {
|
it("should include feedback button when platform is Portal", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons.length).toBe(3);
|
expect(buttons.length).toBe(4);
|
||||||
|
|
||||||
const feedbackButton = buttons[2];
|
const feedbackButton = buttons[3];
|
||||||
expect(feedbackButton).toBeDefined();
|
expect(feedbackButton).toBeDefined();
|
||||||
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
|
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
|
||||||
expect(feedbackButton.tooltipText).toBe("Feedback");
|
expect(feedbackButton.tooltipText).toBe("Feedback");
|
||||||
@@ -105,13 +105,13 @@ describe("CommandBar Utils", () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
|
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
|
||||||
const buttons = getCommandBarButtonsEmulator(mockExplorer);
|
const buttons = getCommandBarButtonsEmulator(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons.length).toBe(2);
|
expect(buttons.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call openCreateCopyJobPanel when create button is clicked", () => {
|
it("should call openCreateCopyJobPanel when create button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const createButton = buttons[0];
|
const createButton = buttons[0];
|
||||||
|
|
||||||
createButton.onCommandClick({} as React.SyntheticEvent);
|
createButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
@@ -121,7 +121,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call refreshJobList when refresh button is clicked", () => {
|
it("should call refreshJobList when refresh button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
refreshButton.onCommandClick({} as React.SyntheticEvent);
|
refreshButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
@@ -130,8 +130,8 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
|
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const feedbackButton = buttons[2];
|
const feedbackButton = buttons[3];
|
||||||
|
|
||||||
feedbackButton.onCommandClick({} as React.SyntheticEvent);
|
feedbackButton.onCommandClick({} as React.SyntheticEvent);
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return buttons with correct icon sources", () => {
|
it("should return buttons with correct icon sources", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons[0].iconSrc).toBeDefined();
|
expect(buttons[0].iconSrc).toBeDefined();
|
||||||
expect(buttons[0].iconAlt).toBe("Create Copy Job");
|
expect(buttons[0].iconAlt).toBe("Create Copy Job");
|
||||||
@@ -148,7 +148,10 @@ describe("CommandBar Utils", () => {
|
|||||||
expect(buttons[1].iconAlt).toBe("Refresh");
|
expect(buttons[1].iconAlt).toBe("Refresh");
|
||||||
|
|
||||||
expect(buttons[2].iconSrc).toBeDefined();
|
expect(buttons[2].iconSrc).toBeDefined();
|
||||||
expect(buttons[2].iconAlt).toBe("Feedback");
|
expect(buttons[2].iconAlt).toBe("Dark Theme");
|
||||||
|
|
||||||
|
expect(buttons[3].iconSrc).toBeDefined();
|
||||||
|
expect(buttons[3].iconAlt).toBe("Feedback");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
|
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
|
||||||
@@ -157,14 +160,14 @@ describe("CommandBar Utils", () => {
|
|||||||
return selector(state);
|
return selector(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
const refreshButton = buttons[1];
|
const refreshButton = buttons[1];
|
||||||
|
|
||||||
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set hasPopup to false for all buttons", () => {
|
it("should set hasPopup to false for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.hasPopup).toBe(false);
|
expect(button.hasPopup).toBe(false);
|
||||||
@@ -172,7 +175,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set commandButtonLabel to undefined for all buttons", () => {
|
it("should set commandButtonLabel to undefined for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.commandButtonLabel).toBeUndefined();
|
expect(button.commandButtonLabel).toBeUndefined();
|
||||||
@@ -180,7 +183,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should respect disabled state when provided", () => {
|
it("should respect disabled state when provided", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.disabled).toBe(false);
|
expect(button.disabled).toBe(false);
|
||||||
@@ -188,7 +191,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return CommandButtonComponentProps with all required properties", () => {
|
it("should return CommandButtonComponentProps with all required properties", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button: CommandButtonComponentProps) => {
|
buttons.forEach((button: CommandButtonComponentProps) => {
|
||||||
expect(button).toHaveProperty("iconSrc");
|
expect(button).toHaveProperty("iconSrc");
|
||||||
@@ -202,18 +205,19 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should maintain button order: create, refresh, feedback", () => {
|
it("should maintain button order: create, refresh, themeToggle, feedback", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
expect(buttons[0].tooltipText).toBe("Create Copy Job");
|
expect(buttons[0].tooltipText).toBe("Create Copy Job");
|
||||||
expect(buttons[1].tooltipText).toBe("Refresh");
|
expect(buttons[1].tooltipText).toBe("Refresh");
|
||||||
expect(buttons[2].tooltipText).toBe("Feedback");
|
expect(buttons[2].tooltipText).toBe("Dark Theme");
|
||||||
|
expect(buttons[3].tooltipText).toBe("Feedback");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Button click handlers", () => {
|
describe("Button click handlers", () => {
|
||||||
it("should execute click handlers without errors", () => {
|
it("should execute click handlers without errors", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
|
||||||
@@ -221,7 +225,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call correct action for each button", () => {
|
it("should call correct action for each button", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons[0].onCommandClick({} as React.SyntheticEvent);
|
buttons[0].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
|
||||||
@@ -229,14 +233,14 @@ describe("CommandBar Utils", () => {
|
|||||||
buttons[1].onCommandClick({} as React.SyntheticEvent);
|
buttons[1].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(mockRefreshJobList).toHaveBeenCalled();
|
expect(mockRefreshJobList).toHaveBeenCalled();
|
||||||
|
|
||||||
buttons[2].onCommandClick({} as React.SyntheticEvent);
|
buttons[3].onCommandClick({} as React.SyntheticEvent);
|
||||||
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
|
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Accessibility", () => {
|
describe("Accessibility", () => {
|
||||||
it("should have aria labels for all buttons", () => {
|
it("should have aria labels for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.ariaLabel).toBeDefined();
|
expect(button.ariaLabel).toBeDefined();
|
||||||
@@ -246,7 +250,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have tooltip text for all buttons", () => {
|
it("should have tooltip text for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.tooltipText).toBeDefined();
|
expect(button.tooltipText).toBeDefined();
|
||||||
@@ -256,7 +260,7 @@ describe("CommandBar Utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have icon alt text for all buttons", () => {
|
it("should have icon alt text for all buttons", () => {
|
||||||
const buttons = getCommandBarButtons(mockExplorer);
|
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||||
|
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
expect(button.iconAlt).toBeDefined();
|
expect(button.iconAlt).toBeDefined();
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import AddIcon from "../../../../images/Add.svg";
|
import AddIcon from "../../../../images/Add.svg";
|
||||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||||
|
import MoonIcon from "../../../../images/MoonIcon.svg";
|
||||||
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
||||||
|
import SunIcon from "../../../../images/SunIcon.svg";
|
||||||
import { configContext, Platform } from "../../../ConfigContext";
|
import { configContext, Platform } from "../../../ConfigContext";
|
||||||
|
import { useThemeStore } from "../../../hooks/useTheme";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import * as Actions from "../Actions/CopyJobActions";
|
import * as Actions from "../Actions/CopyJobActions";
|
||||||
@@ -9,7 +12,7 @@ import ContainerCopyMessages from "../ContainerCopyMessages";
|
|||||||
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||||
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
||||||
|
|
||||||
function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
|
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
|
||||||
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
|
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
|
||||||
const buttons: CopyJobCommandBarBtnType[] = [
|
const buttons: CopyJobCommandBarBtnType[] = [
|
||||||
{
|
{
|
||||||
@@ -26,7 +29,15 @@ function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
|
|||||||
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
||||||
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "themeToggle",
|
||||||
|
iconSrc: isDarkMode ? SunIcon : MoonIcon,
|
||||||
|
label: isDarkMode ? "Light Theme" : "Dark Theme",
|
||||||
|
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme",
|
||||||
|
onClick: () => useThemeStore.getState().toggleTheme(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (configContext.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
key: "feedback",
|
key: "feedback",
|
||||||
@@ -54,6 +65,6 @@ function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProp
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommandBarButtons(explorer: Explorer): CommandButtonComponentProps[] {
|
export function getCommandBarButtons(explorer: Explorer, isDarkMode: boolean): CommandButtonComponentProps[] {
|
||||||
return getCopyJobBtns(explorer).map(btnMapper);
|
return getCopyJobBtns(explorer, isDarkMode).map(btnMapper);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,18 @@ export default {
|
|||||||
subscriptionDropdownPlaceholder: "Select a subscription",
|
subscriptionDropdownPlaceholder: "Select a subscription",
|
||||||
sourceAccountDropdownLabel: "Account",
|
sourceAccountDropdownLabel: "Account",
|
||||||
sourceAccountDropdownPlaceholder: "Select an account",
|
sourceAccountDropdownPlaceholder: "Select an account",
|
||||||
migrationTypeCheckboxLabel: "Copy container in offline mode",
|
migrationTypeOptions: {
|
||||||
|
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:
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const managedIdentityTooltip = (
|
const managedIdentityTooltip = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.addManagedIdentity.tooltip.content}
|
{ContainerCopyMessages.addManagedIdentity.tooltip.content}
|
||||||
<Link href={ContainerCopyMessages.addManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer">
|
<Link
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
href={ContainerCopyMessages.addManagedIdentity.tooltip.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
|
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -26,7 +31,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||||
<Text>
|
<Text className="themeText">
|
||||||
{ContainerCopyMessages.addManagedIdentity.description} 
|
{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}
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const TooltipContent = (
|
const TooltipContent = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
||||||
<Link href={ContainerCopyMessages.readPermissionAssigned.tooltip.href} target="_blank" rel="noopener noreferrer">
|
<Link
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, descripti
|
|||||||
tokens={{ childrenGap: 15 }}
|
tokens={{ childrenGap: 15 }}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
background: "#fafafa",
|
background: "var(--colorNeutralBackground2)",
|
||||||
border: "1px solid #e1e1e1",
|
border: "1px solid var(--colorNeutralStroke1)",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
||||||
@@ -57,11 +57,11 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, descripti
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
<Text variant="medium" style={{ fontWeight: 600 }}>
|
<Text variant="medium" style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{description && (
|
{description && (
|
||||||
<Text variant="small" styles={{ root: { color: "#605E5C" } }}>
|
<Text variant="small" styles={{ root: { color: "var(--colorNeutralForeground2)" } }}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -105,7 +105,7 @@ const AssignPermissions = () => {
|
|||||||
className="assignPermissionsContainer"
|
className="assignPermissionsContainer"
|
||||||
tokens={{ childrenGap: 20 }}
|
tokens={{ childrenGap: 20 }}
|
||||||
>
|
>
|
||||||
<Text variant="medium">
|
<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,7 +12,12 @@ import useToggle from "./hooks/useToggle";
|
|||||||
const managedIdentityTooltip = (
|
const managedIdentityTooltip = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content}
|
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content}
|
||||||
<Link href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer">
|
<Link
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
|
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import InfoTooltip from "../Components/InfoTooltip";
|
|||||||
const tooltipContent = (
|
const tooltipContent = (
|
||||||
<Text>
|
<Text>
|
||||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}
|
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}
|
||||||
<Link href={ContainerCopyMessages.pointInTimeRestore.tooltip.href} target="_blank" rel="noopener noreferrer">
|
<Link
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
href={ContainerCopyMessages.pointInTimeRestore.tooltip.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
|
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you 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.
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you 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.
|
||||||
|
|
||||||
@@ -196,13 +196,13 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="css-124"
|
class="themeText css-124"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Enable system assigned managed identity
|
Enable system assigned managed identity
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
||||||
</span>
|
</span>
|
||||||
@@ -265,7 +265,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
class="ms-Stack addManagedIdentityContainer css-109"
|
class="ms-Stack addManagedIdentityContainer css-109"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you 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.
|
||||||
|
|
||||||
@@ -351,13 +351,13 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-124"
|
class="themeText css-124"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Enable system assigned managed identity
|
Enable system assigned managed identity
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
|
|||||||
style={{ maxWidth: 450 }}
|
style={{ maxWidth: 450 }}
|
||||||
>
|
>
|
||||||
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
|
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
|
||||||
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>
|
<Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>{children}</Text>
|
<Text className="themeText">{children}</Text>
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
|
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
|
||||||
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
|
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -76,7 +76,7 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
@@ -139,7 +139,7 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
@@ -202,13 +202,13 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
This is a very long title that might cause layout issues or text wrapping in the popover component
|
This is a very long title that might cause layout issues or text wrapping in the popover component
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -274,13 +274,13 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -344,13 +344,13 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
@@ -419,13 +419,13 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
|
|||||||
style="max-width: 450px;"
|
style="max-width: 450px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Custom Title
|
Custom Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
@@ -493,13 +493,13 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
|
|||||||
data-testid="loading-overlay"
|
data-testid="loading-overlay"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="css-110"
|
class="themeText css-110"
|
||||||
style="font-weight: 600;"
|
style="font-weight: 600;"
|
||||||
>
|
>
|
||||||
Test Title
|
Test Title
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Test content
|
Test content
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
|||||||
return (
|
return (
|
||||||
<Stack className="addCollectionPanelWrapper">
|
<Stack className="addCollectionPanelWrapper">
|
||||||
<Stack.Item className="addCollectionPanelHeader">
|
<Stack.Item className="addCollectionPanelHeader">
|
||||||
<Text>{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item className="addCollectionPanelBody">
|
<Stack.Item className="addCollectionPanelBody">
|
||||||
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -50,7 +50,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -91,7 +91,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
@@ -132,7 +132,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
|||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="css-111"
|
class="themeText css-111"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Select the properties for your container.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -36,12 +36,16 @@ const PreviewCopyJob: React.FC = () => {
|
|||||||
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||||
<Text data-test="source-subscription-name">{copyJobState.source?.subscription?.displayName}</Text>
|
<Text data-test="source-subscription-name" className="themeText">
|
||||||
|
{copyJobState.source?.subscription?.displayName}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||||
<Text data-test="source-account-name">{copyJobState.source?.account?.name}</Text>
|
<Text data-test="source-account-name" className="themeText">
|
||||||
|
{copyJobState.source?.account?.name}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<DetailsList
|
<DetailsList
|
||||||
|
|||||||
@@ -47,12 +47,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -62,12 +62,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -369,12 +369,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -384,12 +384,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -691,12 +691,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -706,12 +706,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -1013,12 +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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
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
|
||||||
@@ -1028,12 +1028,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
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
|
||||||
@@ -1335,12 +1335,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -1350,7 +1350,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
@@ -1651,7 +1651,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1660,12 +1660,12 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -1967,12 +1967,12 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -1982,12 +1982,12 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
class="ms-Stack css-124"
|
class="ms-Stack css-124"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="bold css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -2289,12 +2289,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -2304,12 +2304,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
@@ -2611,12 +2611,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Source subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="source-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
@@ -2626,12 +2626,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 css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Source account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="source-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/* 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/* 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" data-test="migration-type-checkbox">
|
|
||||||
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
|
|
||||||
</Stack>
|
|
||||||
));
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
|
|
||||||
<div
|
|
||||||
class="ms-Stack migrationTypeRow css-109"
|
|
||||||
data-test="migration-type-checkbox"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="ms-Checkbox is-checked is-enabled root-119"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
data-test="migration-type-checkbox"
|
|
||||||
>
|
|
||||||
<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 { fireEvent, render, screen } from "@testing-library/react";
|
import { 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,19 +18,8 @@ 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/MigrationTypeCheckbox", () => ({
|
jest.mock("./Components/MigrationType", () => ({
|
||||||
MigrationTypeCheckbox: jest.fn(({ checked, onChange }: { checked: boolean; onChange: () => void }) => (
|
MigrationType: jest.fn(() => <div data-testid="migration-type">Migration Type</div>),
|
||||||
<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", () => {
|
||||||
@@ -83,7 +72,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-checkbox")).toBeInTheDocument();
|
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render correctly with snapshot", () => {
|
it("should render correctly with snapshot", () => {
|
||||||
@@ -93,78 +82,20 @@ describe("SelectAccount", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Migration Type Functionality", () => {
|
describe("Migration Type Functionality", () => {
|
||||||
it("should display migration type checkbox as unchecked when migrationType is Online", () => {
|
it("should render migration type component", () => {
|
||||||
(useCopyJobContext as jest.Mock).mockReturnValue({
|
|
||||||
...defaultContextValue,
|
|
||||||
copyJobState: {
|
|
||||||
...defaultContextValue.copyJobState,
|
|
||||||
migrationType: CopyJobMigrationType.Online,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<SelectAccount />);
|
render(<SelectAccount />);
|
||||||
|
|
||||||
const checkbox = screen.getByTestId("migration-checkbox-input");
|
const migrationTypeComponent = screen.getByTestId("migration-type");
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(migrationTypeComponent).toBeInTheDocument();
|
||||||
});
|
|
||||||
|
|
||||||
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 maintain referential equality of handler functions between renders", async () => {
|
it("should render without performance issues", () => {
|
||||||
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 />);
|
||||||
|
|
||||||
const secondRenderHandler = migrationCheckbox.mock.calls[migrationCheckbox.mock.calls.length - 1][0].onChange;
|
expect(screen.getByTestId("migration-type")).toBeInTheDocument();
|
||||||
|
|
||||||
expect(firstRenderHandler).toBe(secondRenderHandler);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
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 { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
|
import { MigrationType } from "./Components/MigrationType";
|
||||||
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>{ContainerCopyMessages.selectAccountDescription}</Text>
|
<Text className="themeText">{ContainerCopyMessages.selectAccountDescription}</Text>
|
||||||
|
|
||||||
<SubscriptionDropdown />
|
<SubscriptionDropdown />
|
||||||
|
|
||||||
<AccountDropdown />
|
<AccountDropdown />
|
||||||
|
|
||||||
<MigrationTypeCheckbox checked={migrationTypeChecked} onChange={handleMigrationTypeChange} />
|
<MigrationType />
|
||||||
</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="css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
Please select a source account from which to copy.
|
Please select a source account from which to copy.
|
||||||
</span>
|
</span>
|
||||||
@@ -21,14 +21,9 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
|||||||
Account Dropdown
|
Account Dropdown
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-testid="migration-type-checkbox"
|
data-testid="migration-type"
|
||||||
>
|
>
|
||||||
<input
|
Migration Type
|
||||||
aria-label="Migration Type Checkbox"
|
|
||||||
data-testid="migration-checkbox-input"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
Copy container in offline mode
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
|||||||
className="selectSourceAndTargetContainers"
|
className="selectSourceAndTargetContainers"
|
||||||
tokens={{ childrenGap: 25 }}
|
tokens={{ childrenGap: 25 }}
|
||||||
>
|
>
|
||||||
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
||||||
<DatabaseContainerSection
|
<DatabaseContainerSection
|
||||||
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
||||||
databaseOptions={sourceDatabaseOptions}
|
databaseOptions={sourceDatabaseOptions}
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ export const DatabaseContainerSection = ({
|
|||||||
data-test={`${sectionType}-containerDropdown`}
|
data-test={`${sectionType}-containerDropdown`}
|
||||||
/>
|
/>
|
||||||
{handleOnDemandCreateContainer && (
|
{handleOnDemandCreateContainer && (
|
||||||
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
|
<ActionButton
|
||||||
|
className="create-container-link-btn"
|
||||||
|
style={{ color: "var(--colorBrandForeground1)" }}
|
||||||
|
onClick={() => handleOnDemandCreateContainer()}
|
||||||
|
>
|
||||||
{ContainerCopyMessages.createContainerButtonLabel}
|
{ContainerCopyMessages.createContainerButtonLabel}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
|
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
|
||||||
import React, { memo } from "react";
|
import React, { memo } from "react";
|
||||||
|
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
import { CopyJobType } from "../../Types/CopyJobTypes";
|
import { CopyJobType } from "../../Types/CopyJobTypes";
|
||||||
@@ -63,6 +64,19 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
||||||
|
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||||
|
|
||||||
|
const errorMessageStyle: React.CSSProperties = {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
...(isDarkMode && {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
backgroundColor: "var(--colorNeutralBackground2)",
|
||||||
|
color: "var(--colorNeutralForeground1)",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
const selectedContainers = [
|
const selectedContainers = [
|
||||||
{
|
{
|
||||||
sourceContainerName: job?.Source?.containerName || "N/A",
|
sourceContainerName: job?.Source?.containerName || "N/A",
|
||||||
@@ -77,10 +91,10 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
|||||||
<Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details">
|
<Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details">
|
||||||
{job.Error ? (
|
{job.Error ? (
|
||||||
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
|
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold" style={sectionCss.headingText}>
|
<Text className="bold themeText" style={sectionCss.headingText}>
|
||||||
{ContainerCopyMessages.errorTitle}
|
{ContainerCopyMessages.errorTitle}
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="pre" style={{ whiteSpace: "pre-wrap" }}>
|
<Text as="pre" style={errorMessageStyle}>
|
||||||
{job.Error.message}
|
{job.Error.message}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@@ -88,16 +102,16 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
|||||||
<Stack.Item data-testid="selectedcollection-stack">
|
<Stack.Item data-testid="selectedcollection-stack">
|
||||||
<Stack tokens={{ childrenGap: 15 }}>
|
<Stack tokens={{ childrenGap: 15 }}>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
|
||||||
<Text>{job.LastUpdatedTime}</Text>
|
<Text className="themeText">{job.LastUpdatedTime}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||||
<Text>{job.Source?.remoteAccountName}</Text>
|
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
||||||
<Text>{job.Mode}</Text>
|
<Text className="themeText">{job.Mode}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|||||||
@@ -1,30 +1,14 @@
|
|||||||
import { FontIcon, getTheme, mergeStyles, mergeStyleSets, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||||
|
|
||||||
const theme = getTheme();
|
|
||||||
|
|
||||||
const iconClass = mergeStyles({
|
const iconClass = mergeStyles({
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
marginRight: "8px",
|
marginRight: "8px",
|
||||||
});
|
});
|
||||||
|
|
||||||
const classNames = mergeStyleSets({
|
|
||||||
[CopyJobStatusType.Pending]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
|
||||||
[CopyJobStatusType.InProgress]: [{ color: theme.palette.themePrimary }, iconClass],
|
|
||||||
[CopyJobStatusType.Running]: [{ color: theme.palette.themePrimary }, iconClass],
|
|
||||||
[CopyJobStatusType.Partitioning]: [{ color: theme.palette.themePrimary }, iconClass],
|
|
||||||
[CopyJobStatusType.Paused]: [{ color: theme.palette.themePrimary }, iconClass],
|
|
||||||
[CopyJobStatusType.Skipped]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
|
||||||
[CopyJobStatusType.Cancelled]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
|
||||||
[CopyJobStatusType.Failed]: [{ color: theme.semanticColors.errorIcon }, iconClass],
|
|
||||||
[CopyJobStatusType.Faulted]: [{ color: theme.semanticColors.errorIcon }, iconClass],
|
|
||||||
[CopyJobStatusType.Completed]: [{ color: theme.semanticColors.successIcon }, iconClass],
|
|
||||||
unknown: [{ color: theme.semanticColors.bodySubtext }, iconClass],
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
||||||
[CopyJobStatusType.Pending]: "Clock",
|
[CopyJobStatusType.Pending]: "Clock",
|
||||||
[CopyJobStatusType.Paused]: "CirclePause",
|
[CopyJobStatusType.Paused]: "CirclePause",
|
||||||
@@ -35,6 +19,17 @@ const iconMap: Partial<Record<CopyJobStatusType, string>> = {
|
|||||||
[CopyJobStatusType.Completed]: "CompletedSolid",
|
[CopyJobStatusType.Completed]: "CompletedSolid",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Icon colors for different statuses
|
||||||
|
const statusIconColors: Partial<Record<CopyJobStatusType, string>> = {
|
||||||
|
[CopyJobStatusType.Failed]: "var(--colorPaletteRedForeground1)",
|
||||||
|
[CopyJobStatusType.Faulted]: "var(--colorPaletteRedForeground1)",
|
||||||
|
[CopyJobStatusType.Completed]: "var(--colorSuccessGreen)",
|
||||||
|
[CopyJobStatusType.InProgress]: "var(--colorBrandForeground1)",
|
||||||
|
[CopyJobStatusType.Running]: "var(--colorBrandForeground1)",
|
||||||
|
[CopyJobStatusType.Partitioning]: "var(--colorBrandForeground1)",
|
||||||
|
[CopyJobStatusType.Paused]: "var(--colorBrandForeground1)",
|
||||||
|
};
|
||||||
|
|
||||||
export interface CopyJobStatusWithIconProps {
|
export interface CopyJobStatusWithIconProps {
|
||||||
status: CopyJobStatusType;
|
status: CopyJobStatusType;
|
||||||
}
|
}
|
||||||
@@ -47,19 +42,17 @@ const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo((
|
|||||||
CopyJobStatusType.InProgress,
|
CopyJobStatusType.InProgress,
|
||||||
CopyJobStatusType.Partitioning,
|
CopyJobStatusType.Partitioning,
|
||||||
].includes(status);
|
].includes(status);
|
||||||
|
const iconColor = statusIconColors[status] || "var(--colorNeutralForeground2)";
|
||||||
|
const iconStyle = mergeStyles(iconClass, { color: iconColor });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
{isSpinnerStatus ? (
|
{isSpinnerStatus ? (
|
||||||
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
|
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
|
||||||
) : (
|
) : (
|
||||||
<FontIcon
|
<FontIcon aria-label={status} iconName={iconMap[status] || "UnknownSolid"} className={iconStyle} />
|
||||||
aria-label={status}
|
|
||||||
iconName={iconMap[status] || "UnknownSolid"}
|
|
||||||
className={classNames[status] || classNames.unknown}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Text>{statusText}</Text>
|
<Text className="themeText">{statusText}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import Pager from "../../../../Common/Pager";
|
import Pager from "../../../../Common/Pager";
|
||||||
|
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||||
|
import { getThemeTokens } from "../../../Theme/ThemeUtil";
|
||||||
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
|
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
|
||||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||||
import { getColumns } from "./CopyJobColumns";
|
import { getColumns } from "./CopyJobColumns";
|
||||||
@@ -26,13 +28,15 @@ interface CopyJobsListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
container: { height: "calc(100vh - 25em)" } as React.CSSProperties,
|
container: { height: "100%" } as React.CSSProperties,
|
||||||
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
|
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
|
||||||
|
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||||
|
const themeTokens = getThemeTokens(isDarkMode);
|
||||||
const [startIndex, setStartIndex] = React.useState(0);
|
const [startIndex, setStartIndex] = React.useState(0);
|
||||||
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
|
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
|
||||||
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
||||||
@@ -88,11 +92,28 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
|||||||
enableShimmer={false}
|
enableShimmer={false}
|
||||||
constrainMode={ConstrainMode.unconstrained}
|
constrainMode={ConstrainMode.unconstrained}
|
||||||
layoutMode={DetailsListLayoutMode.justified}
|
layoutMode={DetailsListLayoutMode.justified}
|
||||||
onRenderDetailsHeader={(props, defaultRender) => (
|
onRenderDetailsHeader={(props, defaultRender) => {
|
||||||
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
|
const bgColor = themeTokens.colorNeutralBackground3;
|
||||||
{defaultRender({ ...props })}
|
const textColor = themeTokens.colorNeutralForeground1;
|
||||||
</Sticky>
|
return (
|
||||||
)}
|
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced stickyBackgroundColor={bgColor}>
|
||||||
|
<div style={{ backgroundColor: bgColor }}>
|
||||||
|
{defaultRender({
|
||||||
|
...props,
|
||||||
|
styles: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
selectors: {
|
||||||
|
".ms-DetailsHeader-cellTitle": { color: textColor },
|
||||||
|
".ms-DetailsHeader-cellName": { color: textColor },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Sticky>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ScrollablePane>
|
</ScrollablePane>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spin
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -33,7 +33,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with sp
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -53,7 +53,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
@@ -66,7 +66,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Cancelled"
|
aria-label="Cancelled"
|
||||||
class="ms-Icon root-105 css-118 mocked-style-Cancelled"
|
class="ms-Icon root-105 css-118 mocked-styles"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -74,7 +74,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Cancelled
|
Cancelled
|
||||||
</span>
|
</span>
|
||||||
@@ -87,7 +87,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Completed"
|
aria-label="Completed"
|
||||||
class="ms-Icon root-105 css-120 mocked-style-Completed"
|
class="ms-Icon root-105 css-120 mocked-styles"
|
||||||
data-icon-name="CompletedSolid"
|
data-icon-name="CompletedSolid"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-5";"
|
style="font-family: "FabricMDL2Icons-5";"
|
||||||
@@ -95,7 +95,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Completed
|
Completed
|
||||||
</span>
|
</span>
|
||||||
@@ -108,7 +108,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Failed"
|
aria-label="Failed"
|
||||||
class="ms-Icon root-105 css-118 mocked-style-Failed"
|
class="ms-Icon root-105 css-118 mocked-styles"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -116,7 +116,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Failed
|
Failed
|
||||||
</span>
|
</span>
|
||||||
@@ -129,7 +129,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Faulted"
|
aria-label="Faulted"
|
||||||
class="ms-Icon root-105 css-118 mocked-style-Faulted"
|
class="ms-Icon root-105 css-118 mocked-styles"
|
||||||
data-icon-name="StatusErrorFull"
|
data-icon-name="StatusErrorFull"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-4";"
|
style="font-family: "FabricMDL2Icons-4";"
|
||||||
@@ -137,7 +137,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Failed
|
Failed
|
||||||
</span>
|
</span>
|
||||||
@@ -150,7 +150,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Paused"
|
aria-label="Paused"
|
||||||
class="ms-Icon root-105 css-114 mocked-style-Paused"
|
class="ms-Icon root-105 css-114 mocked-styles"
|
||||||
data-icon-name="CirclePause"
|
data-icon-name="CirclePause"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-11";"
|
style="font-family: "FabricMDL2Icons-11";"
|
||||||
@@ -158,7 +158,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Paused
|
Paused
|
||||||
</span>
|
</span>
|
||||||
@@ -171,7 +171,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Pending"
|
aria-label="Pending"
|
||||||
class="ms-Icon root-105 css-111 mocked-style-Pending"
|
class="ms-Icon root-105 css-111 mocked-styles"
|
||||||
data-icon-name="Clock"
|
data-icon-name="Clock"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-2";"
|
style="font-family: "FabricMDL2Icons-2";"
|
||||||
@@ -179,7 +179,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Queued
|
Queued
|
||||||
</span>
|
</span>
|
||||||
@@ -192,7 +192,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Skipped"
|
aria-label="Skipped"
|
||||||
class="ms-Icon root-105 css-116 mocked-style-Skipped"
|
class="ms-Icon root-105 css-116 mocked-styles"
|
||||||
data-icon-name="StatusCircleBlock2"
|
data-icon-name="StatusCircleBlock2"
|
||||||
role="img"
|
role="img"
|
||||||
style="font-family: "FabricMDL2Icons-9";"
|
style="font-family: "FabricMDL2Icons-9";"
|
||||||
@@ -200,7 +200,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
|
|||||||
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="css-112"
|
class="themeText css-112"
|
||||||
>
|
>
|
||||||
Cancelled
|
Cancelled
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import CopyJobsNotFound from "../MonitorCopyJobs/Components/CopyJobs.NotFound";
|
|||||||
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
import { CopyJobType, JobActionUpdatorType } from "../Types/CopyJobTypes";
|
||||||
import CopyJobsList from "./Components/CopyJobsList";
|
import CopyJobsList from "./Components/CopyJobsList";
|
||||||
|
|
||||||
const FETCH_INTERVAL_MS = 30 * 1000;
|
const FETCH_INTERVAL = 2 * 60 * 1000;
|
||||||
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
const SHIMMER_INDENT_LEVELS: IndentLevel[] = Array(7).fill({ level: 0, width: "100%" });
|
||||||
|
|
||||||
interface MonitorCopyJobsProps {
|
interface MonitorCopyJobsProps {
|
||||||
@@ -57,7 +57,7 @@ const MonitorCopyJobs = forwardRef<MonitorCopyJobsRef, MonitorCopyJobsProps>(({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchJobs();
|
fetchJobs();
|
||||||
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL_MS);
|
const intervalId = setInterval(fetchJobs, FETCH_INTERVAL);
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [fetchJobs]);
|
}, [fetchJobs]);
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
@import "../../../less/Common/Constants.less";
|
@import "../../../less/Common/Constants.less";
|
||||||
|
|
||||||
|
// Common theme-aware classes
|
||||||
|
.themeText {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeTextSecondary {
|
||||||
|
color: var(--colorNeutralForeground2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeLinkText {
|
||||||
|
color: var(--colorBrandForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeBackground {
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.themeBackgroundSecondary {
|
||||||
|
background-color: var(--colorNeutralBackground2);
|
||||||
|
}
|
||||||
|
|
||||||
#containerCopyWrapper {
|
#containerCopyWrapper {
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
|
||||||
.centerContent {
|
.centerContent {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -9,20 +33,30 @@
|
|||||||
.noCopyJobsMessage {
|
.noCopyJobsMessage {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
color: @FocusColor;
|
color: var(--colorNeutralForeground2);
|
||||||
}
|
}
|
||||||
button.createCopyJobButton {
|
button.createCopyJobButton {
|
||||||
color: @LinkColor;
|
color: var(--colorBrandForeground1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.createCopyJobScreensContainer {
|
.createCopyJobScreensContainer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1em 1.5em;
|
padding: 1em 1.5em;
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
|
||||||
.pointInTimeRestoreContainer, .onlineCopyContainer {
|
.pointInTimeRestoreContainer, .onlineCopyContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeaderText {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -71,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
.foreground {
|
.foreground {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: #f9f9f9;
|
background-color: var(--colorNeutralBackground2);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
transform: translate(0%, -9%);
|
transform: translate(0%, -9%);
|
||||||
@@ -80,14 +114,48 @@
|
|||||||
.createCopyJobErrorMessageBar {
|
.createCopyJobErrorMessageBar {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
body.isDarkMode & {
|
||||||
|
.ms-TooltipHost .ms-Image {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-TextField {
|
||||||
|
.ms-TextField-fieldGroup {
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
border-color: var(--colorNeutralStroke1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-TextField-field {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--colorNeutralForeground4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-Label {
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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: @LinkColor;
|
color: var(--colorBrandForeground1);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create collection panel */
|
/* Create collection panel */
|
||||||
@@ -105,7 +173,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
.ms-DetailsList {
|
.ms-DetailsList {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -114,33 +181,36 @@
|
|||||||
padding: @DefaultSpace 20px;
|
padding: @DefaultSpace 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: @BaseHigh;
|
color: var(--colorNeutralForeground1);
|
||||||
background-color: @BaseLow;
|
background-color: var(--colorNeutralBackground2);
|
||||||
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: @BaseMediumLow;
|
background-color: var(--colorNeutralBackground3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ms-DetailsHeader-cellTitle {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ms-DetailsRow {
|
.ms-DetailsRow {
|
||||||
border-bottom: @ButtonBorderWidth solid @BaseMedium;
|
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: @BaseMediumLow;
|
background-color: var(--colorNeutralBackground2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ms-DetailsRow-cell {
|
.ms-DetailsRow-cell {
|
||||||
padding: @MediumSpace 20px;
|
padding: @MediumSpace 20px;
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: @BaseHigh;
|
color: var(--colorNeutralForeground1);
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.jobNameLink {
|
.jobNameLink {
|
||||||
color: @LinkColor;
|
color: var(--colorBrandForeground1);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -168,7 +238,7 @@
|
|||||||
}
|
}
|
||||||
.ms-DetailsRow-cell {
|
.ms-DetailsRow-cell {
|
||||||
font-size: @DefaultFontSize;
|
font-size: @DefaultFontSize;
|
||||||
color: @BaseHigh;
|
color: var(--colorNeutralForeground1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { IndexingPolicy } from "@azure/cosmos";
|
||||||
|
import { act } from "@testing-library/react";
|
||||||
import { AuthType } from "AuthType";
|
import { 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";
|
||||||
@@ -444,3 +447,49 @@ 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,6 +13,7 @@ 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";
|
||||||
@@ -73,7 +74,6 @@ 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,6 +312,13 @@ 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();
|
||||||
@@ -319,7 +326,11 @@ 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());
|
||||||
@@ -849,7 +860,6 @@ 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 {
|
||||||
@@ -1009,10 +1019,31 @@ 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 ||
|
||||||
@@ -1252,7 +1283,6 @@ 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,7 +155,12 @@ 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 className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
|
<div
|
||||||
|
className="settingsV2Editor"
|
||||||
|
tabIndex={0}
|
||||||
|
ref={this.computedPropertiesDiv}
|
||||||
|
data-test="computed-properties-editor"
|
||||||
|
></div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { MessageBar, MessageBarType } from "@fluentui/react";
|
import { MessageBar, MessageBarType } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||||
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" },
|
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
|
||||||
{ key: GeospatialConfigType.Geometry, text: "Geometry" },
|
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
|
||||||
];
|
];
|
||||||
|
|
||||||
private getGeoSpatialComponent = (): JSX.Element => (
|
private getGeoSpatialComponent = (): JSX.Element => (
|
||||||
|
|||||||
@@ -76,11 +76,11 @@ describe("ThroughputBucketsComponent", () => {
|
|||||||
fireEvent.change(input, { target: { value: "70" } });
|
fireEvent.change(input, { target: { value: "70" } });
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
{ id: 1, maxThroughputPercentage: 70, isDefaultBucket: false },
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
{ id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
{ id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
{ id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
{ id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,11 +102,11 @@ describe("ThroughputBucketsComponent", () => {
|
|||||||
fireEvent.change(input2, { target: { value: "80" } });
|
fireEvent.change(input2, { target: { value: "80" } });
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
{ id: 1, maxThroughputPercentage: 70, isDefaultBucket: false },
|
||||||
{ id: 2, maxThroughputPercentage: 80 },
|
{ id: 2, maxThroughputPercentage: 80, isDefaultBucket: false },
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
{ id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
{ id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
{ id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,8 +134,8 @@ describe("ThroughputBucketsComponent", () => {
|
|||||||
<ThroughputBucketsComponent
|
<ThroughputBucketsComponent
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
currentBuckets={[
|
currentBuckets={[
|
||||||
{ id: 1, maxThroughputPercentage: 100 },
|
{ id: 1, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 2, maxThroughputPercentage: 50 },
|
{ id: 2, maxThroughputPercentage: 50, isDefaultBucket: false },
|
||||||
]}
|
]}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -157,21 +157,21 @@ describe("ThroughputBucketsComponent", () => {
|
|||||||
fireEvent.click(toggles[0]);
|
fireEvent.click(toggles[0]);
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||||
{ id: 1, maxThroughputPercentage: 100 },
|
{ id: 1, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
{ id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
{ id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
{ id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
{ id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
fireEvent.click(toggles[0]);
|
fireEvent.click(toggles[0]);
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
{ id: 1, maxThroughputPercentage: 50, isDefaultBucket: false },
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
{ id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
{ id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
{ id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
{ id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
import {
|
||||||
|
Dropdown,
|
||||||
|
Icon,
|
||||||
|
IDropdownOption,
|
||||||
|
Label,
|
||||||
|
Link,
|
||||||
|
Slider,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
|
Toggle,
|
||||||
|
TooltipHost,
|
||||||
|
} from "@fluentui/react";
|
||||||
import { ThroughputBucket } from "Contracts/DataModels";
|
import { ThroughputBucket } from "Contracts/DataModels";
|
||||||
import React, { FC, useEffect, useState } from "react";
|
import React, { FC, useEffect, useState } from "react";
|
||||||
import { isDirty } from "../../SettingsUtils";
|
import { isDirty } from "../../SettingsUtils";
|
||||||
@@ -8,6 +20,7 @@ const MAX_BUCKET_SIZES = 5;
|
|||||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
maxThroughputPercentage: 100,
|
maxThroughputPercentage: 100,
|
||||||
|
isDefaultBucket: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface ThroughputBucketsComponentProps {
|
export interface ThroughputBucketsComponentProps {
|
||||||
@@ -23,19 +36,51 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
|||||||
onBucketsChange,
|
onBucketsChange,
|
||||||
onSaveableChange,
|
onSaveableChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const NoDefaultThroughputSelectedKey: number = -1;
|
||||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
||||||
if (!buckets || buckets.length === 0) {
|
if (!buckets || buckets.length === 0) {
|
||||||
return DEFAULT_BUCKETS;
|
return DEFAULT_BUCKETS;
|
||||||
}
|
}
|
||||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
||||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
const adjustedDefaultBuckets: ThroughputBucket[] = Array.from(
|
||||||
id: i + 1,
|
{ length: maxBuckets },
|
||||||
maxThroughputPercentage: 100,
|
(_, i) =>
|
||||||
|
({
|
||||||
|
id: i + 1,
|
||||||
|
maxThroughputPercentage: 100,
|
||||||
|
isDefaultBucket: false,
|
||||||
|
}) as ThroughputBucket,
|
||||||
|
);
|
||||||
|
|
||||||
|
return adjustedDefaultBuckets.map((defaultBucket: ThroughputBucket) => {
|
||||||
|
const incoming: ThroughputBucket = buckets?.find((bucket) => bucket.id === defaultBucket.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultBucket,
|
||||||
|
...incoming,
|
||||||
|
...(incoming?.isDefaultBucket && { isDefaultBucket: true }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getThroughputBucketOptions = (): IDropdownOption[] => {
|
||||||
|
const noDefaultThroughputBucketSelected: IDropdownOption = {
|
||||||
|
key: NoDefaultThroughputSelectedKey,
|
||||||
|
text: "No Default Throughput Bucket Selected",
|
||||||
|
};
|
||||||
|
|
||||||
|
const throughputBucketOptions: IDropdownOption[] = throughputBuckets.map((bucket) => ({
|
||||||
|
key: bucket.id,
|
||||||
|
text: `Bucket ${bucket.id} - ${bucket.maxThroughputPercentage}%`,
|
||||||
|
disabled: bucket.maxThroughputPercentage === 100,
|
||||||
|
...(bucket.maxThroughputPercentage === 100 && {
|
||||||
|
data: {
|
||||||
|
tooltip: `Bucket ${bucket.id} is not active.`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return adjustedDefaultBuckets.map(
|
return [noDefaultThroughputBucketSelected, ...throughputBucketOptions];
|
||||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
||||||
@@ -52,7 +97,13 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
|||||||
|
|
||||||
const handleBucketChange = (id: number, newValue: number) => {
|
const handleBucketChange = (id: number, newValue: number) => {
|
||||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
const updatedBuckets = throughputBuckets.map((bucket) =>
|
||||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
bucket.id === id
|
||||||
|
? {
|
||||||
|
...bucket,
|
||||||
|
maxThroughputPercentage: newValue,
|
||||||
|
isDefaultBucket: newValue === 100 ? false : bucket.isDefaultBucket,
|
||||||
|
}
|
||||||
|
: bucket,
|
||||||
);
|
);
|
||||||
setThroughputBuckets(updatedBuckets);
|
setThroughputBuckets(updatedBuckets);
|
||||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
||||||
@@ -63,6 +114,35 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
|||||||
handleBucketChange(id, checked ? 50 : 100);
|
handleBucketChange(id, checked ? 50 : 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDefaultBucketToggle = (id: number, checked: boolean): void => {
|
||||||
|
const updatedBuckets: ThroughputBucket[] = throughputBuckets.map((bucket) =>
|
||||||
|
bucket.id === id ? { ...bucket, isDefaultBucket: checked } : { ...bucket, isDefaultBucket: false },
|
||||||
|
);
|
||||||
|
setThroughputBuckets(updatedBuckets);
|
||||||
|
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
||||||
|
settingsChanged && onBucketsChange(updatedBuckets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRenderDefaultThroughputBucketLabel = (): JSX.Element => {
|
||||||
|
const tooltipContent = (): JSX.Element => (
|
||||||
|
<Text>
|
||||||
|
The default throughput bucket is used for operations that do not specify a particular bucket.{" "}
|
||||||
|
<Link href="https://aka.ms/cosmsodb-bucketing" target="_blank">
|
||||||
|
Learn more.
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack horizontal verticalAlign="center">
|
||||||
|
<Label>Default Throughput Bucket</Label>
|
||||||
|
<TooltipHost content={tooltipContent()}>
|
||||||
|
<Icon iconName="Info" styles={{ root: { marginLeft: 4, marginTop: 5 } }} />
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
||||||
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>Throughput Buckets</Label>
|
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>Throughput Buckets</Label>
|
||||||
@@ -97,17 +177,58 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
|||||||
fieldGroup: { width: 80 },
|
fieldGroup: { width: 80 },
|
||||||
}}
|
}}
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
disabled={bucket.maxThroughputPercentage === 100}
|
||||||
|
data-test={`bucket-${bucket.id}-percentage-input`}
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
onText="Active"
|
onText="Active"
|
||||||
offText="Inactive"
|
offText="Inactive"
|
||||||
checked={bucket.maxThroughputPercentage !== 100}
|
checked={bucket.maxThroughputPercentage !== 100}
|
||||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
styles={{
|
||||||
|
root: { marginBottom: 0 },
|
||||||
|
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||||
|
}}
|
||||||
|
data-test={`bucket-${bucket.id}-active-toggle`}
|
||||||
></Toggle>
|
></Toggle>
|
||||||
|
{/* <Toggle
|
||||||
|
onText="Default"
|
||||||
|
offText="Not Default"
|
||||||
|
checked={bucket.isDefaultBucket || false}
|
||||||
|
onChange={(_, checked) => onDefaultBucketToggle(bucket.id, checked)}
|
||||||
|
disabled={bucket.maxThroughputPercentage === 100}
|
||||||
|
styles={{
|
||||||
|
root: { marginBottom: 0 },
|
||||||
|
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Dropdown
|
||||||
|
placeholder="Select a default throughput bucket"
|
||||||
|
label="Default Throughput Bucket"
|
||||||
|
options={getThroughputBucketOptions()}
|
||||||
|
selectedKey={
|
||||||
|
throughputBuckets?.find((throughputbucket: ThroughputBucket) => throughputbucket.isDefaultBucket)?.id ||
|
||||||
|
NoDefaultThroughputSelectedKey
|
||||||
|
}
|
||||||
|
onChange={(_, option) => onDefaultBucketToggle(option.key as number, true)}
|
||||||
|
onRenderLabel={onRenderDefaultThroughputBucketLabel}
|
||||||
|
onRenderOption={(option: IDropdownOption) => {
|
||||||
|
const tooltip: string = option?.data?.tooltip;
|
||||||
|
if (!tooltip) {
|
||||||
|
return <>{option?.text}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipHost content={tooltip}>
|
||||||
|
<span>{option?.text}</span>
|
||||||
|
</TooltipHost>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
styles={{ root: { width: "50%" } }}
|
||||||
|
data-test="default-throughput-bucket-dropdown"
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
|
|||||||
</Text>
|
</Text>
|
||||||
<div
|
<div
|
||||||
className="settingsV2Editor"
|
className="settingsV2Editor"
|
||||||
|
data-test="computed-properties-editor"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -167,10 +167,12 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geography-option",
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geometry-option",
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
@@ -652,10 +654,12 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geography-option",
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geometry-option",
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
@@ -1224,10 +1228,12 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geography-option",
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geometry-option",
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
@@ -1760,10 +1766,12 @@ exports[`SubSettingsComponent renders 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geography-option",
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geometry-option",
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
@@ -2330,10 +2338,12 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
|||||||
options={
|
options={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geography-option",
|
||||||
"key": "Geography",
|
"key": "Geography",
|
||||||
"text": "Geography",
|
"text": "Geography",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ariaLabel": "geometry-option",
|
||||||
"key": "Geometry",
|
"key": "Geometry",
|
||||||
"text": "Geometry",
|
"text": "Geometry",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -153,6 +153,16 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
"rawDataModel": {
|
||||||
|
"indexingPolicy": {
|
||||||
|
"automatic": true,
|
||||||
|
"compositeIndexes": [],
|
||||||
|
"excludedPaths": [],
|
||||||
|
"fullTextIndexes": [],
|
||||||
|
"includedPaths": [],
|
||||||
|
"indexingMode": "consistent",
|
||||||
|
"spatialIndexes": [],
|
||||||
|
"vectorIndexes": [],
|
||||||
|
},
|
||||||
"uniqueKeyPolicy": {
|
"uniqueKeyPolicy": {
|
||||||
"uniqueKeys": [
|
"uniqueKeys": [
|
||||||
{
|
{
|
||||||
@@ -264,6 +274,16 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
"rawDataModel": {
|
||||||
|
"indexingPolicy": {
|
||||||
|
"automatic": true,
|
||||||
|
"compositeIndexes": [],
|
||||||
|
"excludedPaths": [],
|
||||||
|
"fullTextIndexes": [],
|
||||||
|
"includedPaths": [],
|
||||||
|
"indexingMode": "consistent",
|
||||||
|
"spatialIndexes": [],
|
||||||
|
"vectorIndexes": [],
|
||||||
|
},
|
||||||
"uniqueKeyPolicy": {
|
"uniqueKeyPolicy": {
|
||||||
"uniqueKeys": [
|
"uniqueKeys": [
|
||||||
{
|
{
|
||||||
@@ -476,6 +496,16 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
"rawDataModel": {
|
||||||
|
"indexingPolicy": {
|
||||||
|
"automatic": true,
|
||||||
|
"compositeIndexes": [],
|
||||||
|
"excludedPaths": [],
|
||||||
|
"fullTextIndexes": [],
|
||||||
|
"includedPaths": [],
|
||||||
|
"indexingMode": "consistent",
|
||||||
|
"spatialIndexes": [],
|
||||||
|
"vectorIndexes": [],
|
||||||
|
},
|
||||||
"uniqueKeyPolicy": {
|
"uniqueKeyPolicy": {
|
||||||
"uniqueKeys": [
|
"uniqueKeys": [
|
||||||
{
|
{
|
||||||
@@ -653,6 +683,16 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKey",
|
"partitionKey",
|
||||||
],
|
],
|
||||||
"rawDataModel": {
|
"rawDataModel": {
|
||||||
|
"indexingPolicy": {
|
||||||
|
"automatic": true,
|
||||||
|
"compositeIndexes": [],
|
||||||
|
"excludedPaths": [],
|
||||||
|
"fullTextIndexes": [],
|
||||||
|
"includedPaths": [],
|
||||||
|
"indexingMode": "consistent",
|
||||||
|
"spatialIndexes": [],
|
||||||
|
"vectorIndexes": [],
|
||||||
|
},
|
||||||
"uniqueKeyPolicy": {
|
"uniqueKeyPolicy": {
|
||||||
"uniqueKeys": [
|
"uniqueKeys": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexTyp
|
|||||||
const labelStyles = {
|
const labelStyles = {
|
||||||
root: {
|
root: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
color: "var(--colorNeutralForeground1)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,6 +64,8 @@ const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldSt
|
|||||||
field: {
|
field: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
padding: "0 8px",
|
padding: "0 8px",
|
||||||
|
backgroundColor: "var(--colorNeutralBackground1)",
|
||||||
|
color: "var(--colorNeutralForeground1)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -853,7 +853,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{!isSynapseLinkEnabled() && (
|
{!isSynapseLinkEnabled() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text variant="small">
|
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||||
Azure Synapse Link is required for creating an analytical store{" "}
|
Azure Synapse Link is required for creating an analytical store{" "}
|
||||||
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -475,6 +475,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "var(--colorNeutralForeground1)",
|
||||||
|
}
|
||||||
|
}
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
Azure Synapse Link is required for creating an analytical store
|
Azure Synapse Link is required for creating an analytical store
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
|
||||||
export const PanelLoadingScreen: React.FunctionComponent = () => (
|
export const PanelLoadingScreen: React.FunctionComponent = () => (
|
||||||
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
|
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerLoaderforcopyJobs">
|
||||||
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
|||||||
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
|
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
|
||||||
/>
|
/>
|
||||||
{uploadFileData?.length > 0 && (
|
{uploadFileData?.length > 0 && (
|
||||||
<div className="fileUploadSummaryContainer">
|
<div className="fileUploadSummaryContainer" data-test="file-upload-status">
|
||||||
<b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b>
|
<b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b>
|
||||||
<DetailsList
|
<DetailsList
|
||||||
items={uploadFileData}
|
items={uploadFileData}
|
||||||
|
|||||||
107
src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.tsx
Normal file
107
src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { CircleFilled } from "@fluentui/react-icons";
|
||||||
|
import type { IIndexMetric } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||||
|
import { useIndexAdvisorStyles } from "Explorer/Tabs/QueryTab/StylesAdvisor";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
// SDK response format
|
||||||
|
export interface IndexMetricsResponse {
|
||||||
|
UtilizedIndexes?: {
|
||||||
|
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
|
||||||
|
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
|
||||||
|
};
|
||||||
|
PotentialIndexes?: {
|
||||||
|
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
|
||||||
|
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseIndexMetrics(indexMetrics: IndexMetricsResponse): {
|
||||||
|
included: IIndexMetric[];
|
||||||
|
notIncluded: IIndexMetric[];
|
||||||
|
} {
|
||||||
|
const included: IIndexMetric[] = [];
|
||||||
|
const notIncluded: IIndexMetric[] = [];
|
||||||
|
|
||||||
|
// Process UtilizedIndexes (Included in Current Policy)
|
||||||
|
if (indexMetrics.UtilizedIndexes) {
|
||||||
|
// Single indexes
|
||||||
|
indexMetrics.UtilizedIndexes.SingleIndexes?.forEach((index) => {
|
||||||
|
included.push({
|
||||||
|
index: index.IndexSpec,
|
||||||
|
impact: index.IndexImpactScore || "Utilized",
|
||||||
|
section: "Included",
|
||||||
|
path: index.IndexSpec,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Composite indexes
|
||||||
|
indexMetrics.UtilizedIndexes.CompositeIndexes?.forEach((index) => {
|
||||||
|
const compositeSpec = index.IndexSpecs.join(", ");
|
||||||
|
included.push({
|
||||||
|
index: compositeSpec,
|
||||||
|
impact: index.IndexImpactScore || "Utilized",
|
||||||
|
section: "Included",
|
||||||
|
composite: index.IndexSpecs.map((spec) => {
|
||||||
|
const [path, order] = spec.trim().split(/\s+/);
|
||||||
|
return {
|
||||||
|
path: path.trim(),
|
||||||
|
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process PotentialIndexes (Not Included in Current Policy)
|
||||||
|
if (indexMetrics.PotentialIndexes) {
|
||||||
|
// Single indexes
|
||||||
|
indexMetrics.PotentialIndexes.SingleIndexes?.forEach((index) => {
|
||||||
|
notIncluded.push({
|
||||||
|
index: index.IndexSpec,
|
||||||
|
impact: index.IndexImpactScore || "Unknown",
|
||||||
|
section: "Not Included",
|
||||||
|
path: index.IndexSpec,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Composite indexes
|
||||||
|
indexMetrics.PotentialIndexes.CompositeIndexes?.forEach((index) => {
|
||||||
|
const compositeSpec = index.IndexSpecs.join(", ");
|
||||||
|
notIncluded.push({
|
||||||
|
index: compositeSpec,
|
||||||
|
impact: index.IndexImpactScore || "Unknown",
|
||||||
|
section: "Not Included",
|
||||||
|
composite: index.IndexSpecs.map((spec) => {
|
||||||
|
const [path, order] = spec.trim().split(/\s+/);
|
||||||
|
return {
|
||||||
|
path: path.trim(),
|
||||||
|
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { included, notIncluded };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderImpactDots = (impact: string): JSX.Element => {
|
||||||
|
const style = useIndexAdvisorStyles();
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
if (impact === "High") {
|
||||||
|
count = 3;
|
||||||
|
} else if (impact === "Medium") {
|
||||||
|
count = 2;
|
||||||
|
} else if (impact === "Low") {
|
||||||
|
count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.indexAdvisorImpactDots}>
|
||||||
|
{Array.from({ length: count }).map((_, i) => (
|
||||||
|
<CircleFilled key={i} className={style.indexAdvisorImpactDot} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,18 +3,21 @@ import QueryError from "Common/QueryError";
|
|||||||
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
|
import { 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 {
|
||||||
@@ -49,6 +52,8 @@ 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);
|
||||||
@@ -91,6 +96,9 @@ 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,8 +52,9 @@ describe("QueryTabComponent", () => {
|
|||||||
copilotVersion: "v3.0",
|
copilotVersion: "v3.0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const propsMock: Readonly<IQueryTabComponentProps> = {
|
const propsMock: Readonly<IQueryTabComponentProps> = {
|
||||||
collection: { databaseId: "CopilotSampleDB" },
|
collection: { databaseId: "CopilotSampleDB", id: () => "CopilotContainer" },
|
||||||
onTabAccessor: () => jest.fn(),
|
onTabAccessor: () => jest.fn(),
|
||||||
isExecutionError: false,
|
isExecutionError: false,
|
||||||
tabId: "mockTabId",
|
tabId: "mockTabId",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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";
|
||||||
@@ -57,6 +58,20 @@ 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,
|
||||||
@@ -264,6 +279,10 @@ 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 () => {
|
||||||
@@ -780,6 +799,8 @@ 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,
|
||||||
@@ -795,6 +816,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
170
src/Explorer/Tabs/QueryTab/ResultsView.test.tsx
Normal file
170
src/Explorer/Tabs/QueryTab/ResultsView.test.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { IndexAdvisorTab } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const mockReplace = jest.fn();
|
||||||
|
const mockFetchAll = jest.fn();
|
||||||
|
const mockRead = jest.fn();
|
||||||
|
const mockLogConsoleProgress = jest.fn();
|
||||||
|
const mockHandleError = jest.fn();
|
||||||
|
|
||||||
|
const indexMetricsResponse = {
|
||||||
|
UtilizedIndexes: {
|
||||||
|
SingleIndexes: [{ IndexSpec: "/foo/?", IndexImpactScore: "High" }],
|
||||||
|
CompositeIndexes: [{ IndexSpecs: ["/baz/? DESC", "/qux/? ASC"], IndexImpactScore: "Low" }],
|
||||||
|
},
|
||||||
|
PotentialIndexes: {
|
||||||
|
SingleIndexes: [{ IndexSpec: "/bar/?", IndexImpactScore: "Medium" }],
|
||||||
|
CompositeIndexes: [] as Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockQueryResults = {
|
||||||
|
documents: [] as unknown[],
|
||||||
|
hasMoreResults: false,
|
||||||
|
itemCount: 0,
|
||||||
|
firstItemIndex: 0,
|
||||||
|
lastItemIndex: 0,
|
||||||
|
requestCharge: 0,
|
||||||
|
activityId: "test-activity-id",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRead.mockResolvedValue({
|
||||||
|
resource: {
|
||||||
|
indexingPolicy: {
|
||||||
|
automatic: true,
|
||||||
|
indexingMode: "consistent",
|
||||||
|
includedPaths: [{ path: "/*" }, { path: "/foo/?" }],
|
||||||
|
excludedPaths: [],
|
||||||
|
},
|
||||||
|
partitionKey: "pk",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockReplace.mockResolvedValue({
|
||||||
|
resource: {
|
||||||
|
indexingPolicy: {
|
||||||
|
automatic: true,
|
||||||
|
indexingMode: "consistent",
|
||||||
|
includedPaths: [{ path: "/*" }],
|
||||||
|
excludedPaths: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("Common/CosmosClient", () => ({
|
||||||
|
client: () => ({
|
||||||
|
database: () => ({
|
||||||
|
container: () => ({
|
||||||
|
items: {
|
||||||
|
query: () => ({
|
||||||
|
fetchAll: mockFetchAll,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
read: mockRead,
|
||||||
|
replace: mockReplace,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("./StylesAdvisor", () => ({
|
||||||
|
useIndexAdvisorStyles: () => ({}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../../Utils/NotificationConsoleUtils", () => ({
|
||||||
|
logConsoleProgress: (...args: unknown[]) => {
|
||||||
|
mockLogConsoleProgress(...args);
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../../Common/ErrorHandlingUtils", () => ({
|
||||||
|
handleError: (...args: unknown[]) => mockHandleError(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockFetchAll.mockResolvedValue({ indexMetrics: indexMetricsResponse });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("IndexAdvisorTab Basic Tests", () => {
|
||||||
|
test("component renders without crashing", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<IndexAdvisorTab queryEditorContent="SELECT * FROM c" databaseId="db1" containerId="col1" />,
|
||||||
|
);
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders component and handles missing parameters", () => {
|
||||||
|
const { container } = render(<IndexAdvisorTab />);
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
// Should not crash when parameters are missing
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fetches index metrics with query results", async () => {
|
||||||
|
render(
|
||||||
|
<IndexAdvisorTab
|
||||||
|
queryResults={mockQueryResults}
|
||||||
|
queryEditorContent="SELECT * FROM c"
|
||||||
|
databaseId="db1"
|
||||||
|
containerId="col1"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("displays content after loading", async () => {
|
||||||
|
render(
|
||||||
|
<IndexAdvisorTab
|
||||||
|
queryResults={mockQueryResults}
|
||||||
|
queryEditorContent="SELECT * FROM c"
|
||||||
|
databaseId="db1"
|
||||||
|
containerId="col1"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// Wait for the component to finish loading
|
||||||
|
await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
|
||||||
|
// Component should have rendered some content
|
||||||
|
expect(screen.getByText(/Index Advisor/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls log console progress when fetching metrics", async () => {
|
||||||
|
render(
|
||||||
|
<IndexAdvisorTab
|
||||||
|
queryResults={mockQueryResults}
|
||||||
|
queryEditorContent="SELECT * FROM c"
|
||||||
|
databaseId="db1"
|
||||||
|
containerId="col1"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(mockLogConsoleProgress).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles error when fetch fails", async () => {
|
||||||
|
mockFetchAll.mockRejectedValueOnce(new Error("fetch failed"));
|
||||||
|
render(
|
||||||
|
<IndexAdvisorTab
|
||||||
|
queryResults={mockQueryResults}
|
||||||
|
queryEditorContent="SELECT * FROM c"
|
||||||
|
databaseId="db1"
|
||||||
|
containerId="col1"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(mockHandleError).toHaveBeenCalled(), { timeout: 3000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders with all required props", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<IndexAdvisorTab
|
||||||
|
queryResults={mockQueryResults}
|
||||||
|
queryEditorContent="SELECT * FROM c"
|
||||||
|
databaseId="testDb"
|
||||||
|
containerId="testContainer"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
expect(container.firstChild).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import type { CompositePath, IndexingPolicy } from "@azure/cosmos";
|
||||||
|
import { FontIcon } from "@fluentui/react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
DataGrid,
|
DataGrid,
|
||||||
DataGridBody,
|
DataGridBody,
|
||||||
DataGridCell,
|
DataGridCell,
|
||||||
@@ -8,28 +11,45 @@ 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, CopyRegular } from "@fluentui/react-icons";
|
import { ArrowDownloadRegular, ChevronDown20Regular, ChevronRight20Regular, CopyRegular } from "@fluentui/react-icons";
|
||||||
|
import copy from "clipboard-copy";
|
||||||
import { HttpHeaders } from "Common/Constants";
|
import { 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 copy from "clipboard-copy";
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
import React, { useCallback, useState } from "react";
|
import create from "zustand";
|
||||||
|
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 */
|
||||||
@@ -523,14 +543,331 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
|
export interface IIndexMetric {
|
||||||
|
index: string;
|
||||||
|
impact: string;
|
||||||
|
section: "Included" | "Not Included" | "Header";
|
||||||
|
path?: string;
|
||||||
|
composite?: { path: string; order: string }[];
|
||||||
|
}
|
||||||
|
export const IndexAdvisorTab: React.FC<{
|
||||||
|
queryResults?: QueryResults;
|
||||||
|
queryEditorContent?: string;
|
||||||
|
databaseId?: string;
|
||||||
|
containerId?: string;
|
||||||
|
}> = ({ queryResults, queryEditorContent, databaseId, containerId }) => {
|
||||||
|
const style = useIndexAdvisorStyles();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [indexMetrics, setIndexMetrics] = useState<IndexMetricsResponse | null>(null);
|
||||||
|
const [showIncluded, setShowIncluded] = useState(true);
|
||||||
|
const [showNotIncluded, setShowNotIncluded] = useState(true);
|
||||||
|
const [selectedIndexes, setSelectedIndexes] = useState<IIndexMetric[]>([]);
|
||||||
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
const [updateMessageShown, setUpdateMessageShown] = useState(false);
|
||||||
|
const [included, setIncludedIndexes] = useState<IIndexMetric[]>([]);
|
||||||
|
const [notIncluded, setNotIncludedIndexes] = useState<IIndexMetric[]>([]);
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
|
||||||
|
const indexingMetricsDocLink = "https://learn.microsoft.com/azure/cosmos-db/nosql/index-metrics";
|
||||||
|
|
||||||
|
const fetchIndexMetrics = async () => {
|
||||||
|
if (!queryEditorContent || !databaseId || !containerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
|
||||||
|
try {
|
||||||
|
const querySpec = {
|
||||||
|
query: queryEditorContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use sampleDataClient for CopilotSampleDB, regular client for other databases
|
||||||
|
const cosmosClient = databaseId === "CopilotSampleDB" ? sampleDataClient() : client();
|
||||||
|
|
||||||
|
const sdkResponse = await cosmosClient
|
||||||
|
.database(databaseId)
|
||||||
|
.container(containerId)
|
||||||
|
.items.query(querySpec, {
|
||||||
|
populateIndexMetrics: true,
|
||||||
|
})
|
||||||
|
.fetchAll();
|
||||||
|
|
||||||
|
const parsedMetrics =
|
||||||
|
typeof sdkResponse.indexMetrics === "string" ? JSON.parse(sdkResponse.indexMetrics) : sdkResponse.indexMetrics;
|
||||||
|
|
||||||
|
setIndexMetrics(parsedMetrics);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`);
|
||||||
|
} finally {
|
||||||
|
clearMessage();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch index metrics when query results change (i.e., when Execute Query is clicked)
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryEditorContent && databaseId && containerId && queryResults) {
|
||||||
|
fetchIndexMetrics();
|
||||||
|
}
|
||||||
|
}, [queryResults]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!indexMetrics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { included, notIncluded } = parseIndexMetrics(indexMetrics);
|
||||||
|
setIncludedIndexes(included);
|
||||||
|
setNotIncludedIndexes(notIncluded);
|
||||||
|
if (justUpdatedPolicy) {
|
||||||
|
setJustUpdatedPolicy(false);
|
||||||
|
} else {
|
||||||
|
setUpdateMessageShown(false);
|
||||||
|
}
|
||||||
|
}, [indexMetrics]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allSelected =
|
||||||
|
notIncluded.length > 0 && notIncluded.every((item) => selectedIndexes.some((s) => s.index === item.index));
|
||||||
|
setSelectAll(allSelected);
|
||||||
|
}, [selectedIndexes, notIncluded]);
|
||||||
|
|
||||||
|
const handleCheckboxChange = (indexObj: IIndexMetric, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedIndexes((prev) => [...prev, indexObj]);
|
||||||
|
} else {
|
||||||
|
setSelectedIndexes((prev) => prev.filter((item) => item.index !== indexObj.index));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
setSelectAll(checked);
|
||||||
|
setSelectedIndexes(checked ? notIncluded : []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdatePolicy = async () => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
const containerRef = client().database(databaseId).container(containerId);
|
||||||
|
const { resource: containerDef } = await containerRef.read();
|
||||||
|
|
||||||
|
const newIncludedPaths = selectedIndexes
|
||||||
|
.filter((index) => !index.composite)
|
||||||
|
.map((index) => {
|
||||||
|
return {
|
||||||
|
path: index.path,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const newCompositeIndexes: CompositePath[][] = selectedIndexes
|
||||||
|
.filter((index) => Array.isArray(index.composite))
|
||||||
|
.map(
|
||||||
|
(index) =>
|
||||||
|
(index.composite as { path: string; order: string }[]).map((comp) => ({
|
||||||
|
path: comp.path,
|
||||||
|
order: comp.order === "descending" ? "descending" : "ascending",
|
||||||
|
})) as CompositePath[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedPolicy: IndexingPolicy = {
|
||||||
|
...containerDef.indexingPolicy,
|
||||||
|
includedPaths: [...(containerDef.indexingPolicy?.includedPaths || []), ...newIncludedPaths],
|
||||||
|
compositeIndexes: [...(containerDef.indexingPolicy?.compositeIndexes || []), ...newCompositeIndexes],
|
||||||
|
automatic: containerDef.indexingPolicy?.automatic ?? true,
|
||||||
|
indexingMode: containerDef.indexingPolicy?.indexingMode ?? "consistent",
|
||||||
|
excludedPaths: containerDef.indexingPolicy?.excludedPaths ?? [],
|
||||||
|
};
|
||||||
|
await containerRef.replace({
|
||||||
|
id: containerId,
|
||||||
|
partitionKey: containerDef.partitionKey,
|
||||||
|
indexingPolicy: updatedPolicy,
|
||||||
|
});
|
||||||
|
useIndexingPolicyStore.getState().setIndexingPolicyFor(containerId, updatedPolicy);
|
||||||
|
const selectedIndexSet = new Set(selectedIndexes.map((s) => s.index));
|
||||||
|
const updatedNotIncluded: typeof notIncluded = [];
|
||||||
|
const newlyIncluded: typeof included = [];
|
||||||
|
for (const item of notIncluded) {
|
||||||
|
if (selectedIndexSet.has(item.index)) {
|
||||||
|
newlyIncluded.push(item);
|
||||||
|
} else {
|
||||||
|
updatedNotIncluded.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newIncluded = [...included, ...newlyIncluded];
|
||||||
|
const newNotIncluded = updatedNotIncluded;
|
||||||
|
setIncludedIndexes(newIncluded);
|
||||||
|
setNotIncludedIndexes(newNotIncluded);
|
||||||
|
setSelectedIndexes([]);
|
||||||
|
setSelectAll(false);
|
||||||
|
setUpdateMessageShown(true);
|
||||||
|
setJustUpdatedPolicy(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update indexing policy:", err);
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRow = (item: IIndexMetric, index: number) => {
|
||||||
|
const isHeader = item.section === "Header";
|
||||||
|
const isNotIncluded = item.section === "Not Included";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell colSpan={2}>
|
||||||
|
<div className={style.indexAdvisorGrid}>
|
||||||
|
{isNotIncluded ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIndexes.some((selected) => selected.index === item.index)}
|
||||||
|
onChange={(_, data) => handleCheckboxChange(item, data.checked === true)}
|
||||||
|
/>
|
||||||
|
) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? (
|
||||||
|
<Checkbox checked={selectAll} onChange={(_, data) => handleSelectAll(data.checked === true)} />
|
||||||
|
) : (
|
||||||
|
<div className={style.indexAdvisorCheckboxSpacer}></div>
|
||||||
|
)}
|
||||||
|
{isHeader ? (
|
||||||
|
<span
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={() => {
|
||||||
|
if (item.index === "Included in Current Policy") {
|
||||||
|
setShowIncluded(!showIncluded);
|
||||||
|
} else if (item.index === "Not Included in Current Policy") {
|
||||||
|
setShowNotIncluded(!showNotIncluded);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.index === "Included in Current Policy" ? (
|
||||||
|
showIncluded ? (
|
||||||
|
<ChevronDown20Regular />
|
||||||
|
) : (
|
||||||
|
<ChevronRight20Regular />
|
||||||
|
)
|
||||||
|
) : showNotIncluded ? (
|
||||||
|
<ChevronDown20Regular />
|
||||||
|
) : (
|
||||||
|
<ChevronRight20Regular />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div className={style.indexAdvisorChevronSpacer}></div>
|
||||||
|
)}
|
||||||
|
<div className={isHeader ? style.indexAdvisorRowBold : style.indexAdvisorRowNormal}>{item.index}</div>
|
||||||
|
<div className={isHeader ? style.indexAdvisorRowImpactHeader : style.indexAdvisorRowImpact}>
|
||||||
|
{!isHeader && item.impact}
|
||||||
|
</div>
|
||||||
|
<div>{!isHeader && renderImpactDots(item.impact)}</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const indexMetricItems = React.useMemo(() => {
|
||||||
|
const items: IIndexMetric[] = [];
|
||||||
|
items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" });
|
||||||
|
if (showNotIncluded) {
|
||||||
|
notIncluded.forEach((item) => items.push({ ...item, section: "Not Included" }));
|
||||||
|
}
|
||||||
|
items.push({ index: "Included in Current Policy", impact: "", section: "Header" });
|
||||||
|
if (showIncluded) {
|
||||||
|
included.forEach((item) => items.push({ ...item, section: "Included" }));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}, [included, notIncluded, showIncluded, showNotIncluded]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Spinner
|
||||||
|
size="small"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--spinner-size": "16px",
|
||||||
|
"--spinner-thickness": "2px",
|
||||||
|
"--spinner-color": "#0078D4",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={style.indexAdvisorMessage}>
|
||||||
|
{updateMessageShown ? (
|
||||||
|
<>
|
||||||
|
<span className={style.indexAdvisorSuccessIcon}>
|
||||||
|
<FontIcon iconName="CheckMark" style={{ color: "white", fontSize: 12 }} />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Your indexing policy has been updated with the new included paths. You may review the changes in Scale &
|
||||||
|
Settings.
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
Index Advisor uses Indexing Metrics to suggest query paths that, when included in your indexing policy,
|
||||||
|
can improve the performance of this query by reducing RU costs and lowering latency.{" "}
|
||||||
|
<a href={indexingMetricsDocLink} target="_blank" rel="noopener noreferrer">
|
||||||
|
Learn more about Indexing Metrics
|
||||||
|
</a>
|
||||||
|
.{" "}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={style.indexAdvisorTitle}>Indexes analysis</div>
|
||||||
|
<Table className={style.indexAdvisorTable}>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2}>
|
||||||
|
<div className={style.indexAdvisorGrid}>
|
||||||
|
<div className={style.indexAdvisorCheckboxSpacer}></div>
|
||||||
|
<div className={style.indexAdvisorChevronSpacer}></div>
|
||||||
|
<div>Index</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ whiteSpace: "nowrap" }}>Estimated Impact</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>{indexMetricItems.map(renderRow)}</TableBody>
|
||||||
|
</Table>
|
||||||
|
{selectedIndexes.length > 0 && (
|
||||||
|
<div className={style.indexAdvisorButtonBar}>
|
||||||
|
{isUpdating ? (
|
||||||
|
<div className={style.indexAdvisorButtonSpinner}>
|
||||||
|
<Spinner size="tiny" />{" "}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button onClick={handleUpdatePolicy} className={style.indexAdvisorButton}>
|
||||||
|
Update Indexing Policy with selected index(es)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const ResultsView: React.FC<ResultsViewProps> = ({
|
||||||
|
isMongoDB,
|
||||||
|
queryResults,
|
||||||
|
executeQueryDocumentsPage,
|
||||||
|
queryEditorContent,
|
||||||
|
databaseId,
|
||||||
|
containerId,
|
||||||
|
}) => {
|
||||||
const styles = useQueryTabStyles();
|
const 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}>
|
||||||
@@ -548,6 +885,13 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
|||||||
>
|
>
|
||||||
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 && (
|
||||||
@@ -558,7 +902,30 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{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 },
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|||||||
95
src/Explorer/Tabs/QueryTab/StylesAdvisor.ts
Normal file
95
src/Explorer/Tabs/QueryTab/StylesAdvisor.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { makeStyles } from "@fluentui/react-components";
|
||||||
|
export type IndexAdvisorStyles = ReturnType<typeof useIndexAdvisorStyles>;
|
||||||
|
export const useIndexAdvisorStyles = makeStyles({
|
||||||
|
indexAdvisorMessage: {
|
||||||
|
padding: "1rem",
|
||||||
|
fontSize: "1.2rem",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
},
|
||||||
|
indexAdvisorSuccessIcon: {
|
||||||
|
width: "18px",
|
||||||
|
height: "18px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "#107C10",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
indexAdvisorTitle: {
|
||||||
|
padding: "1rem",
|
||||||
|
fontSize: "1.3rem",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
indexAdvisorTable: {
|
||||||
|
display: "block",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "7rem",
|
||||||
|
},
|
||||||
|
indexAdvisorGrid: {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "30px 30px 1fr 50px 120px",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "15px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
indexAdvisorCheckboxSpacer: {
|
||||||
|
width: "18px",
|
||||||
|
height: "18px",
|
||||||
|
},
|
||||||
|
indexAdvisorChevronSpacer: {
|
||||||
|
width: "24px",
|
||||||
|
},
|
||||||
|
indexAdvisorRowBold: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
indexAdvisorRowNormal: {
|
||||||
|
fontWeight: "normal",
|
||||||
|
},
|
||||||
|
indexAdvisorRowImpactHeader: {
|
||||||
|
fontSize: 0,
|
||||||
|
},
|
||||||
|
indexAdvisorRowImpact: {
|
||||||
|
fontWeight: "normal",
|
||||||
|
},
|
||||||
|
indexAdvisorImpactDot: {
|
||||||
|
color: "#0078D4",
|
||||||
|
fontSize: "12px",
|
||||||
|
display: "inline-flex",
|
||||||
|
},
|
||||||
|
indexAdvisorImpactDots: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "4px",
|
||||||
|
},
|
||||||
|
indexAdvisorButtonBar: {
|
||||||
|
padding: "1rem",
|
||||||
|
marginTop: "-7rem",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
},
|
||||||
|
indexAdvisorButtonSpinner: {
|
||||||
|
marginTop: "1rem",
|
||||||
|
minWidth: "320px",
|
||||||
|
minHeight: "40px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "left",
|
||||||
|
justifyContent: "left",
|
||||||
|
marginLeft: "10rem",
|
||||||
|
},
|
||||||
|
indexAdvisorButton: {
|
||||||
|
backgroundColor: "#0078D4",
|
||||||
|
color: "white",
|
||||||
|
padding: "8px 16px",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
marginTop: "1rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: "background 0.2s",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "#005a9e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
15
src/Explorer/Tabs/QueryTab/useQueryMetadataStore.ts
Normal file
15
src/Explorer/Tabs/QueryTab/useQueryMetadataStore.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import create from "zustand";
|
||||||
|
|
||||||
|
interface QueryMetadataStore {
|
||||||
|
userQuery: string;
|
||||||
|
databaseId: string;
|
||||||
|
containerId: string;
|
||||||
|
setMetadata: (query1: string, db: string, container: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
|
||||||
|
userQuery: "",
|
||||||
|
databaseId: "",
|
||||||
|
containerId: "",
|
||||||
|
setMetadata: (query1, db, container) => set({ userQuery: query1, databaseId: db, containerId: container }),
|
||||||
|
}));
|
||||||
@@ -435,7 +435,6 @@ export default class StoredProcedureTabComponent extends React.Component<
|
|||||||
});
|
});
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return createdResource;
|
return createdResource;
|
||||||
},
|
},
|
||||||
(createError) => {
|
(createError) => {
|
||||||
|
|||||||
@@ -598,7 +598,13 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public onSettingsClick = async (): Promise<void> => {
|
public onSettingsClick = async (): Promise<void> => {
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
useSelectedNode.getState().setSelectedNode(this);
|
||||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
|
if (throughputCap && throughputCap !== -1) {
|
||||||
|
await this.container.onRefreshResourcesClick();
|
||||||
|
await useDatabases.getState().loadAllOffers();
|
||||||
|
} else {
|
||||||
|
await this.loadOffer();
|
||||||
|
}
|
||||||
|
// throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
||||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
description: "Settings node",
|
description: "Settings node",
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
|
|
||||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
if (throughputCap && throughputCap !== -1) {
|
if (throughputCap && throughputCap !== -1) {
|
||||||
|
await this.container.onRefreshResourcesClick();
|
||||||
await useDatabases.getState().loadAllOffers();
|
await useDatabases.getState().loadAllOffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Metric, onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
|
import { Metric, onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import { trackEvent } from "../Shared/appInsights";
|
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceFailure, traceMark, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
|
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
|
||||||
import { scenarioConfigs } from "./MetricScenarioConfigs";
|
import { scenarioConfigs } from "./MetricScenarioConfigs";
|
||||||
@@ -83,6 +84,13 @@ class ScenarioMonitor {
|
|||||||
ctx.phases.set(phase, { startMarkName: phaseStartMarkName });
|
ctx.phases.set(phase, { startMarkName: phaseStartMarkName });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
traceMark(Action.MetricsScenario, {
|
||||||
|
event: "scenario_start",
|
||||||
|
scenario,
|
||||||
|
requiredPhases: config.requiredPhases.join(","),
|
||||||
|
timeoutMs: config.timeoutMs,
|
||||||
|
});
|
||||||
|
|
||||||
ctx.timeoutId = window.setTimeout(() => this.emit(ctx, false, true), config.timeoutMs);
|
ctx.timeoutId = window.setTimeout(() => this.emit(ctx, false, true), config.timeoutMs);
|
||||||
this.contexts.set(scenario, ctx);
|
this.contexts.set(scenario, ctx);
|
||||||
}
|
}
|
||||||
@@ -96,6 +104,12 @@ class ScenarioMonitor {
|
|||||||
const startMarkName = `scenario_${scenario}_${phase}_start`;
|
const startMarkName = `scenario_${scenario}_${phase}_start`;
|
||||||
performance.mark(startMarkName);
|
performance.mark(startMarkName);
|
||||||
ctx.phases.set(phase, { startMarkName });
|
ctx.phases.set(phase, { startMarkName });
|
||||||
|
|
||||||
|
traceStart(Action.MetricsScenario, {
|
||||||
|
event: "phase_start",
|
||||||
|
scenario,
|
||||||
|
phase,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
completePhase(scenario: MetricScenario, phase: MetricPhase) {
|
completePhase(scenario: MetricScenario, phase: MetricPhase) {
|
||||||
@@ -110,6 +124,22 @@ class ScenarioMonitor {
|
|||||||
phaseCtx.endMarkName = endMarkName;
|
phaseCtx.endMarkName = endMarkName;
|
||||||
ctx.completed.add(phase);
|
ctx.completed.add(phase);
|
||||||
|
|
||||||
|
const navigationStart = performance.timeOrigin;
|
||||||
|
const startEntry = performance.getEntriesByName(phaseCtx.startMarkName)[0];
|
||||||
|
const endEntry = performance.getEntriesByName(endMarkName)[0];
|
||||||
|
const endTimeISO = endEntry ? new Date(navigationStart + endEntry.startTime).toISOString() : undefined;
|
||||||
|
const durationMs = startEntry && endEntry ? endEntry.startTime - startEntry.startTime : undefined;
|
||||||
|
|
||||||
|
traceSuccess(Action.MetricsScenario, {
|
||||||
|
event: "phase_complete",
|
||||||
|
scenario,
|
||||||
|
phase,
|
||||||
|
endTimeISO,
|
||||||
|
durationMs,
|
||||||
|
completedCount: ctx.completed.size,
|
||||||
|
requiredCount: ctx.config.requiredPhases.length,
|
||||||
|
});
|
||||||
|
|
||||||
this.tryEmitIfReady(ctx);
|
this.tryEmitIfReady(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +163,14 @@ class ScenarioMonitor {
|
|||||||
// Build a snapshot with failure info
|
// Build a snapshot with failure info
|
||||||
const failureSnapshot = this.buildSnapshot(ctx, { final: false, timedOut: false });
|
const failureSnapshot = this.buildSnapshot(ctx, { final: false, timedOut: false });
|
||||||
|
|
||||||
|
traceFailure(Action.MetricsScenario, {
|
||||||
|
event: "phase_fail",
|
||||||
|
scenario,
|
||||||
|
phase,
|
||||||
|
failedPhases: Array.from(ctx.failed).join(","),
|
||||||
|
completedPhases: Array.from(ctx.completed).join(","),
|
||||||
|
});
|
||||||
|
|
||||||
// Emit unhealthy immediately
|
// Emit unhealthy immediately
|
||||||
this.emit(ctx, false, false, failureSnapshot);
|
this.emit(ctx, false, false, failureSnapshot);
|
||||||
}
|
}
|
||||||
@@ -191,27 +229,22 @@ class ScenarioMonitor {
|
|||||||
// Build snapshot if not provided
|
// Build snapshot if not provided
|
||||||
const finalSnapshot = snapshot || this.buildSnapshot(ctx, { final: false, timedOut });
|
const finalSnapshot = snapshot || this.buildSnapshot(ctx, { final: false, timedOut });
|
||||||
|
|
||||||
// Emit enriched telemetry with performance data
|
traceMark(Action.MetricsScenario, {
|
||||||
// TODO: Call portal backend metrics endpoint
|
event: "scenario_end",
|
||||||
trackEvent(
|
scenario: ctx.scenario,
|
||||||
{ name: "MetricScenarioComplete" },
|
healthy,
|
||||||
{
|
timedOut,
|
||||||
scenario: ctx.scenario,
|
platform,
|
||||||
healthy: healthy.toString(),
|
api,
|
||||||
timedOut: timedOut.toString(),
|
durationMs: finalSnapshot.durationMs,
|
||||||
platform,
|
completedPhases: finalSnapshot.completed.join(","),
|
||||||
api,
|
failedPhases: finalSnapshot.failedPhases?.join(","),
|
||||||
durationMs: finalSnapshot.durationMs.toString(),
|
lcp: finalSnapshot.vitals?.lcp,
|
||||||
completedPhases: finalSnapshot.completed.join(","),
|
inp: finalSnapshot.vitals?.inp,
|
||||||
failedPhases: finalSnapshot.failedPhases?.join(","),
|
cls: finalSnapshot.vitals?.cls,
|
||||||
lcp: finalSnapshot.vitals?.lcp?.toString(),
|
fcp: finalSnapshot.vitals?.fcp,
|
||||||
inp: finalSnapshot.vitals?.inp?.toString(),
|
ttfb: finalSnapshot.vitals?.ttfb,
|
||||||
cls: finalSnapshot.vitals?.cls?.toString(),
|
});
|
||||||
fcp: finalSnapshot.vitals?.fcp?.toString(),
|
|
||||||
ttfb: finalSnapshot.vitals?.ttfb?.toString(),
|
|
||||||
phaseTimings: JSON.stringify(finalSnapshot.phaseTimings),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Call portal backend health metrics endpoint
|
// Call portal backend health metrics endpoint
|
||||||
if (healthy && !timedOut) {
|
if (healthy && !timedOut) {
|
||||||
@@ -227,9 +260,16 @@ class ScenarioMonitor {
|
|||||||
private cleanupPerformanceEntries(ctx: InternalScenarioContext) {
|
private cleanupPerformanceEntries(ctx: InternalScenarioContext) {
|
||||||
performance.clearMarks(ctx.startMarkName);
|
performance.clearMarks(ctx.startMarkName);
|
||||||
ctx.config.requiredPhases.forEach((phase) => {
|
ctx.config.requiredPhases.forEach((phase) => {
|
||||||
performance.clearMarks(`scenario_${ctx.scenario}_${phase}`);
|
const phaseCtx = ctx.phases.get(phase);
|
||||||
|
if (phaseCtx?.startMarkName) {
|
||||||
|
performance.clearMarks(phaseCtx.startMarkName);
|
||||||
|
}
|
||||||
|
if (phaseCtx?.endMarkName) {
|
||||||
|
performance.clearMarks(phaseCtx.endMarkName);
|
||||||
|
}
|
||||||
|
performance.clearMarks(`scenario_${ctx.scenario}_${phase}_failed`);
|
||||||
|
performance.clearMeasures(`scenario_${ctx.scenario}_${phase}_duration`);
|
||||||
});
|
});
|
||||||
performance.clearMeasures(`scenario_${ctx.scenario}_total`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildSnapshot(
|
private buildSnapshot(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Some of the enums names are used in Fabric. Please do not rename them.
|
// Some of the enums names are used in Fabric. Please do not rename them.
|
||||||
export enum Action {
|
export enum Action {
|
||||||
CollapseTreeNode,
|
CollapseTreeNode,
|
||||||
|
MetricsScenario,
|
||||||
CreateCollection, // Used in Fabric. Please do not rename.
|
CreateCollection, // Used in Fabric. Please do not rename.
|
||||||
CreateGlobalSecondaryIndex,
|
CreateGlobalSecondaryIndex,
|
||||||
CreateDocument, // Used in Fabric. Please do not rename.
|
CreateDocument, // Used in Fabric. Please do not rename.
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */
|
/* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */
|
||||||
export async function listCassandraKeyspaces(
|
export async function listCassandraKeyspaces(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given database account and collection. */
|
/* Retrieves the metrics determined by the given filter for the given database account and collection. */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given collection, split by partition. */
|
/* Retrieves the metrics determined by the given filter for the given collection, split by partition. */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */
|
/* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given database account, collection and region. */
|
/* Retrieves the metrics determined by the given filter for the given database account, collection and region. */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given database account and database. */
|
/* Retrieves the metrics determined by the given filter for the given database account and database. */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given database account and region. */
|
/* Retrieves the metrics determined by the given filter for the given database account and region. */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the properties of an existing Azure Cosmos DB database account. */
|
/* Retrieves the properties of an existing Azure Cosmos DB database account. */
|
||||||
export async function get(
|
export async function get(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Lists the graphs under an existing Azure Cosmos DB database account. */
|
/* Lists the graphs under an existing Azure Cosmos DB database account. */
|
||||||
export async function listGraphs(
|
export async function listGraphs(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Lists the Gremlin databases under an existing Azure Cosmos DB database account. */
|
/* Lists the Gremlin databases under an existing Azure Cosmos DB database account. */
|
||||||
export async function listGremlinDatabases(
|
export async function listGremlinDatabases(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* List Cosmos DB locations and their properties */
|
/* List Cosmos DB locations and their properties */
|
||||||
export async function list(subscriptionId: string): Promise<Types.LocationListResult | Types.CloudError> {
|
export async function list(subscriptionId: string): Promise<Types.LocationListResult | Types.CloudError> {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Lists the MongoDB databases under an existing Azure Cosmos DB database account. */
|
/* Lists the MongoDB databases under an existing Azure Cosmos DB database account. */
|
||||||
export async function listMongoDBDatabases(
|
export async function listMongoDBDatabases(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Lists all of the available Cosmos DB Resource Provider operations. */
|
/* Lists all of the available Cosmos DB Resource Provider operations. */
|
||||||
export async function list(): Promise<Types.OperationListResult> {
|
export async function list(): Promise<Types.OperationListResult> {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given partition key range id. */
|
/* Retrieves the metrics determined by the given filter for the given partition key range id. */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given partition key range id and region. */
|
/* Retrieves the metrics determined by the given filter for the given partition key range id and region. */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given database account. This url is only for PBS and Replication Latency data */
|
/* Retrieves the metrics determined by the given filter for the given database account. This url is only for PBS and Replication Latency data */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given account, source and target region. This url is only for PBS and Replication Latency data */
|
/* Retrieves the metrics determined by the given filter for the given account, source and target region. This url is only for PBS and Replication Latency data */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Retrieves the metrics determined by the given filter for the given account target region. This url is only for PBS and Replication Latency data */
|
/* Retrieves the metrics determined by the given filter for the given account target region. This url is only for PBS and Replication Latency data */
|
||||||
export async function listMetrics(
|
export async function listMetrics(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Lists the SQL databases under an existing Azure Cosmos DB database account. */
|
/* Lists the SQL databases under an existing Azure Cosmos DB database account. */
|
||||||
export async function listSqlDatabases(
|
export async function listSqlDatabases(
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { armRequest } from "../../request";
|
import { armRequest } from "../../request";
|
||||||
import * as Types from "./types";
|
import * as Types from "./types";
|
||||||
import { configContext } from "../../../../ConfigContext";
|
import { configContext } from "../../../../ConfigContext";
|
||||||
const apiVersion = "2025-05-01-preview";
|
const apiVersion = "2025-11-01-preview";
|
||||||
|
|
||||||
/* Lists the Tables under an existing Azure Cosmos DB database account. */
|
/* Lists the Tables under an existing Azure Cosmos DB database account. */
|
||||||
export async function listTables(
|
export async function listTables(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Run "npm run generateARMClients" to regenerate
|
Run "npm run generateARMClients" to regenerate
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2025-05-01-preview/cosmos-db.json
|
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The List operation response, that contains the client encryption keys and their properties. */
|
/* The List operation response, that contains the client encryption keys and their properties. */
|
||||||
@@ -573,6 +573,8 @@ export interface DatabaseAccountGetProperties {
|
|||||||
|
|
||||||
/* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */
|
/* Indicates the status of the Customer Managed Key feature on the account. In case there are errors, the property provides troubleshooting guidance. */
|
||||||
customerManagedKeyStatus?: string;
|
customerManagedKeyStatus?: string;
|
||||||
|
/* The version of the Customer Managed Key currently being used by the account */
|
||||||
|
readonly keyVaultKeyUriVersion?: string;
|
||||||
/* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */
|
/* Flag to indicate enabling/disabling of Priority Based Execution Preview feature on the account */
|
||||||
enablePriorityBasedExecution?: boolean;
|
enablePriorityBasedExecution?: boolean;
|
||||||
/* Enum to indicate default Priority Level of request for Priority Based Execution. */
|
/* Enum to indicate default Priority Level of request for Priority Based Execution. */
|
||||||
@@ -582,6 +584,10 @@ export interface DatabaseAccountGetProperties {
|
|||||||
enablePerRegionPerPartitionAutoscale?: boolean;
|
enablePerRegionPerPartitionAutoscale?: boolean;
|
||||||
/* Flag to indicate if All Versions and Deletes Change feed feature is enabled on the account */
|
/* Flag to indicate if All Versions and Deletes Change feed feature is enabled on the account */
|
||||||
enableAllVersionsAndDeletesChangeFeed?: boolean;
|
enableAllVersionsAndDeletesChangeFeed?: boolean;
|
||||||
|
/* Total dedicated throughput (RU/s) for database account. Represents the sum of all manual provisioned throughput and all autoscale max RU/s across all shared throughput databases and dedicated throughput containers in the account for 1 region. READ ONLY. */
|
||||||
|
throughputPoolDedicatedRUs?: number;
|
||||||
|
/* When this account is part of a fleetspace with throughput pooling enabled, this is the maximum additional throughput (RU/s) that can be consumed from the pool, summed across all shared throughput databases and dedicated throughput containers in the account for 1 region. READ ONLY. */
|
||||||
|
throughputPoolMaxConsumableRUs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Properties to create and update Azure Cosmos DB database accounts. */
|
/* Properties to create and update Azure Cosmos DB database accounts. */
|
||||||
@@ -1105,7 +1111,7 @@ export interface ThroughputSettingsResource {
|
|||||||
readonly instantMaximumThroughput?: string;
|
readonly instantMaximumThroughput?: string;
|
||||||
/* The maximum throughput value or the maximum maxThroughput value (for autoscale) that can be specified */
|
/* The maximum throughput value or the maximum maxThroughput value (for autoscale) that can be specified */
|
||||||
readonly softAllowedMaximumThroughput?: string;
|
readonly softAllowedMaximumThroughput?: string;
|
||||||
/* Array of Throughput Bucket limits to be applied to the Cosmos DB container */
|
/* Array of throughput bucket limits to be applied to the Cosmos DB container */
|
||||||
throughputBuckets?: ThroughputBucketResource[];
|
throughputBuckets?: ThroughputBucketResource[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1140,6 +1146,8 @@ export interface ThroughputBucketResource {
|
|||||||
id: number;
|
id: number;
|
||||||
/* Represents maximum percentage throughput that can be used by the bucket */
|
/* Represents maximum percentage throughput that can be used by the bucket */
|
||||||
maxThroughputPercentage: number;
|
maxThroughputPercentage: number;
|
||||||
|
/* Indicates whether this is the default throughput bucket */
|
||||||
|
isDefaultBucket?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cosmos DB options resource object */
|
/* Cosmos DB options resource object */
|
||||||
@@ -1296,6 +1304,9 @@ export interface SqlContainerResource {
|
|||||||
/* Materialized Views defined on the container. */
|
/* Materialized Views defined on the container. */
|
||||||
materializedViews?: MaterializedViewDetails[];
|
materializedViews?: MaterializedViewDetails[];
|
||||||
|
|
||||||
|
/* Materialized Views Properties defined for source container. */
|
||||||
|
materializedViewsProperties?: MaterializedViewsProperties;
|
||||||
|
|
||||||
/* List of computed properties */
|
/* List of computed properties */
|
||||||
computedProperties?: ComputedProperty[];
|
computedProperties?: ComputedProperty[];
|
||||||
|
|
||||||
@@ -1304,6 +1315,9 @@ export interface SqlContainerResource {
|
|||||||
|
|
||||||
/* The FullText policy for the container. */
|
/* The FullText policy for the container. */
|
||||||
fullTextPolicy?: FullTextPolicy;
|
fullTextPolicy?: FullTextPolicy;
|
||||||
|
|
||||||
|
/* The Data Masking policy for the container. */
|
||||||
|
dataMaskingPolicy?: DataMaskingPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cosmos DB indexing policy */
|
/* Cosmos DB indexing policy */
|
||||||
@@ -1327,6 +1341,9 @@ export interface IndexingPolicy {
|
|||||||
|
|
||||||
/* List of paths to include in the vector indexing */
|
/* List of paths to include in the vector indexing */
|
||||||
vectorIndexes?: VectorIndex[];
|
vectorIndexes?: VectorIndex[];
|
||||||
|
|
||||||
|
/* List of paths to include in the full text indexing */
|
||||||
|
fullTextIndexes?: FullTextIndexPath[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cosmos DB Vector Embedding Policy */
|
/* Cosmos DB Vector Embedding Policy */
|
||||||
@@ -1374,6 +1391,13 @@ export interface VectorIndex {
|
|||||||
path: string;
|
path: string;
|
||||||
/* The index type of the vector. Currently, flat, diskANN, and quantizedFlat are supported. */
|
/* The index type of the vector. Currently, flat, diskANN, and quantizedFlat are supported. */
|
||||||
type: "flat" | "diskANN" | "quantizedFlat";
|
type: "flat" | "diskANN" | "quantizedFlat";
|
||||||
|
|
||||||
|
/* The number of bytes used in product quantization of the vectors. A larger value may result in better recall for vector searches at the expense of latency. This is only applicable for the quantizedFlat and diskANN vector index types. */
|
||||||
|
quantizationByteSize?: number;
|
||||||
|
/* This is the size of the candidate list of approximate neighbors stored while building the DiskANN index as part of the optimization processes. Large values may improve recall at the expense of latency. This is only applicable for the diskANN vector index type. */
|
||||||
|
indexingSearchListSize?: number;
|
||||||
|
/* Array of shard keys for the vector index. This is only applicable for the quantizedFlat and diskANN vector index types. */
|
||||||
|
vectorIndexShardKey?: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Represents a vector embedding. A vector embedding is used to define a vector field in the documents. */
|
/* Represents a vector embedding. A vector embedding is used to define a vector field in the documents. */
|
||||||
@@ -1381,7 +1405,7 @@ export interface VectorEmbedding {
|
|||||||
/* The path to the vector field in the document. */
|
/* The path to the vector field in the document. */
|
||||||
path: string;
|
path: string;
|
||||||
/* Indicates the data type of vector. */
|
/* Indicates the data type of vector. */
|
||||||
dataType: "float32" | "uint8" | "int8";
|
dataType: "float32" | "uint8" | "int8" | "float16";
|
||||||
|
|
||||||
/* The distance function to use for distance calculation in between vectors. */
|
/* The distance function to use for distance calculation in between vectors. */
|
||||||
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
||||||
@@ -1390,6 +1414,12 @@ export interface VectorEmbedding {
|
|||||||
dimensions: number;
|
dimensions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Represents the full text index path. */
|
||||||
|
export interface FullTextIndexPath {
|
||||||
|
/* The path to the full text field in the document. */
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
/* Represents the full text path specification. */
|
/* Represents the full text path specification. */
|
||||||
export interface FullTextPath {
|
export interface FullTextPath {
|
||||||
/* The path to the full text field in the document. */
|
/* The path to the full text field in the document. */
|
||||||
@@ -1489,6 +1519,18 @@ export interface ClientEncryptionIncludedPath {
|
|||||||
encryptionAlgorithm: string;
|
encryptionAlgorithm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Data masking policy for the container. */
|
||||||
|
export interface DataMaskingPolicy {
|
||||||
|
/* List of JSON paths to include in the masking policy. */
|
||||||
|
includedPaths?: unknown[];
|
||||||
|
|
||||||
|
/* List of JSON paths to exclude from masking. */
|
||||||
|
excludedPaths?: unknown[];
|
||||||
|
|
||||||
|
/* Flag indicating whether the data masking policy is enabled. */
|
||||||
|
isPolicyEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/* Materialized View definition for the container. */
|
/* Materialized View definition for the container. */
|
||||||
export interface MaterializedViewDefinition {
|
export interface MaterializedViewDefinition {
|
||||||
/* An unique identifier for the source collection. This is a system generated property. */
|
/* An unique identifier for the source collection. This is a system generated property. */
|
||||||
@@ -1497,6 +1539,14 @@ export interface MaterializedViewDefinition {
|
|||||||
sourceCollectionId: string;
|
sourceCollectionId: string;
|
||||||
/* The definition should be an SQL query which would be used to fetch data from the source container to populate into the Materialized View container. */
|
/* The definition should be an SQL query which would be used to fetch data from the source container to populate into the Materialized View container. */
|
||||||
definition: string;
|
definition: string;
|
||||||
|
/* Throughput bucket assigned for the materialized view operations on target container. */
|
||||||
|
throughputBucketForBuild?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Materialized Views Properties for the source container. */
|
||||||
|
export interface MaterializedViewsProperties {
|
||||||
|
/* Throughput bucket assigned for the materialized view operations on source container. */
|
||||||
|
throughputBucketForBuild?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MaterializedViewDetails, contains Id & _rid fields of materialized view. */
|
/* MaterializedViewDetails, contains Id & _rid fields of materialized view. */
|
||||||
|
|||||||
@@ -316,11 +316,6 @@ body.isDarkMode {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// High specificity override for any nested elements
|
|
||||||
* {
|
|
||||||
color: var(--colorNeutralForeground1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure links maintain proper colors
|
// Ensure links maintain proper colors
|
||||||
.ms-Link {
|
.ms-Link {
|
||||||
color: var(--colorBrandForeground1);
|
color: var(--colorBrandForeground1);
|
||||||
@@ -438,7 +433,6 @@ body.isDarkMode {
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
&:not(.ms-Button):not(.ms-IconButton) {
|
&:not(.ms-Button):not(.ms-IconButton) {
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
color: var(--colorNeutralForeground1);
|
color: var(--colorNeutralForeground1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
--colorCompoundBrandStroke1: @SelectionColor;
|
--colorCompoundBrandStroke1: @SelectionColor;
|
||||||
--colorBrandForeground1: @LinkColor;
|
--colorBrandForeground1: @LinkColor;
|
||||||
--colorPaletteRedForeground1: @ErrorColor;
|
--colorPaletteRedForeground1: @ErrorColor;
|
||||||
|
--colorSuccessGreen: #107c10;
|
||||||
--overlayBackground: rgba(0, 0, 0, 0.4);
|
--overlayBackground: rgba(0, 0, 0, 0.4);
|
||||||
--colorBrandBackground: @SelectionColor;
|
--colorBrandBackground: @SelectionColor;
|
||||||
--colorBrandBackgroundHover: @AccentMediumHigh;
|
--colorBrandBackgroundHover: @AccentMediumHigh;
|
||||||
@@ -32,6 +33,7 @@ body.isDarkMode {
|
|||||||
--colorCompoundBrandStroke1: #4db6e8;
|
--colorCompoundBrandStroke1: #4db6e8;
|
||||||
--colorBrandForeground1: #4db6e8;
|
--colorBrandForeground1: #4db6e8;
|
||||||
--colorPaletteRedForeground1: #f25d5d;
|
--colorPaletteRedForeground1: #f25d5d;
|
||||||
|
--colorSuccessGreen: #107c10;
|
||||||
--overlayBackground: rgba(0, 0, 0, 0.4);
|
--overlayBackground: rgba(0, 0, 0, 0.4);
|
||||||
--colorBrandBackground: #0078d4;
|
--colorBrandBackground: #0078d4;
|
||||||
--colorBrandBackgroundHover: #106ebe;
|
--colorBrandBackgroundHover: #106ebe;
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ $ENV:NOSQL_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<accou
|
|||||||
# NoSQL API (Readonly)
|
# NoSQL API (Readonly)
|
||||||
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
# NoSQL API (Container Copy)
|
||||||
|
$ENV:NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
# Tables API
|
# Tables API
|
||||||
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user