feat: Redesign container-copy flow to select destination account and enable cross-account container creation (#2436)

* feat: Redesign container-copy flow to select destination account and enable cross-account container creation

* upgrade RBAC permissions from read only to read-write

* fix copyjob playwright tests

* swap source-destination content

* fix formating

* use targetAccountOverride for capability checks in AddCollectionPanel

* feat: localize ContainerCopy hardcoded strings using i18next and refactor readDatabasesWithARM

* removed container copy messages json file
This commit is contained in:
BChoudhury-ms
2026-05-11 17:24:51 +05:30
committed by GitHub
parent 762e12a3f9
commit d374458e1b
81 changed files with 1810 additions and 1076 deletions
+5 -4
View File
@@ -34,6 +34,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
databaseId: params.databaseId, databaseId: params.databaseId,
databaseLevelThroughput: params.databaseLevelThroughput, databaseLevelThroughput: params.databaseLevelThroughput,
offerThroughput: params.offerThroughput, offerThroughput: params.offerThroughput,
targetAccountOverride: params.targetAccountOverride,
}; };
await createDatabase(createDatabaseParams); await createDatabase(createDatabaseParams);
} }
@@ -63,7 +64,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
}; };
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
if (!params.createNewDatabase) { if (!params.createNewDatabase && !params.targetAccountOverride) {
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId); const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
if (!isValid) { if (!isValid) {
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
@@ -122,9 +123,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
}; };
const createResponse = await createUpdateSqlContainer( const createResponse = await createUpdateSqlContainer(
userContext.subscriptionId, params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId,
userContext.resourceGroup, params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup,
userContext.databaseAccount.name, params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name,
params.databaseId, params.databaseId,
params.collectionId, params.collectionId,
rpPayload, rpPayload,
@@ -0,0 +1,137 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
jest.mock("../../Utils/arm/generatedClients/cosmos/sqlResources");
import ko from "knockout";
import { AuthType } from "../../AuthType";
import { CreateDatabaseParams, DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { updateUserContext } from "../../UserContext";
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { SqlDatabaseGetResults } from "../../Utils/arm/generatedClients/cosmos/types";
import { createDatabase } from "./createDatabase";
const mockCreateUpdateSqlDatabase = createUpdateSqlDatabase as jest.MockedFunction<typeof createUpdateSqlDatabase>;
describe("createDatabase", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: { name: "default-account" } as DatabaseAccount,
subscriptionId: "default-subscription",
resourceGroup: "default-rg",
apiType: "SQL",
authType: AuthType.AAD,
});
});
beforeEach(() => {
jest.clearAllMocks();
mockCreateUpdateSqlDatabase.mockResolvedValue({
properties: { resource: { id: "db", _rid: "", _self: "", _ts: 0, _etag: "" } },
} as SqlDatabaseGetResults);
useDatabases.setState({
databases: [],
validateDatabaseId: () => true,
} as unknown as ReturnType<typeof useDatabases.getState>);
});
it("should call ARM createUpdateSqlDatabase when logged in with AAD", async () => {
await createDatabase({ databaseId: "testDb" });
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled();
});
describe("targetAccountOverride behavior", () => {
it("should use targetAccountOverride subscriptionId, resourceGroup, and accountName for SQL DB creation", async () => {
const params: CreateDatabaseParams = {
databaseId: "testDb",
targetAccountOverride: {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
capabilities: [],
},
};
await createDatabase(params);
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
"override-sub",
"override-rg",
"override-account",
"testDb",
expect.any(Object),
);
});
it("should use userContext values when targetAccountOverride is not provided", async () => {
await createDatabase({ databaseId: "testDb" });
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
"default-subscription",
"default-rg",
"default-account",
"testDb",
expect.any(Object),
);
});
it("should skip validateDatabaseId check when targetAccountOverride is provided", async () => {
// Simulate database already existing — validateDatabaseId returns false
useDatabases.setState({
databases: [{ id: ko.observable("testDb") } as unknown as ViewModels.Database],
validateDatabaseId: () => false,
} as unknown as ReturnType<typeof useDatabases.getState>);
const params: CreateDatabaseParams = {
databaseId: "testDb",
targetAccountOverride: {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
capabilities: [],
},
};
// Should NOT throw even though the normal duplicate check would fail
await expect(createDatabase(params)).resolves.not.toThrow();
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled();
});
it("should throw if validateDatabaseId returns false and no targetAccountOverride is set", async () => {
useDatabases.setState({
databases: [{ id: ko.observable("existingDb") } as unknown as ViewModels.Database],
validateDatabaseId: () => false,
} as unknown as ReturnType<typeof useDatabases.getState>);
await expect(createDatabase({ databaseId: "existingDb" })).rejects.toThrow();
expect(mockCreateUpdateSqlDatabase).not.toHaveBeenCalled();
});
it("should pass databaseId in request payload regardless of targetAccountOverride", async () => {
const params: CreateDatabaseParams = {
databaseId: "my-database",
targetAccountOverride: {
subscriptionId: "any-sub",
resourceGroup: "any-rg",
accountName: "any-account",
capabilities: [],
},
};
await createDatabase(params);
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
expect.any(String),
expect.any(String),
expect.any(String),
"my-database",
expect.objectContaining({
properties: expect.objectContaining({
resource: expect.objectContaining({ id: "my-database" }),
}),
}),
);
});
});
});
+5 -8
View File
@@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
} }
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) { if (!params.targetAccountOverride && !useDatabases.getState().validateDatabaseId(params.databaseId)) {
const databaseName = getDatabaseName().toLocaleLowerCase(); const databaseName = getDatabaseName().toLocaleLowerCase();
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`); throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
} }
@@ -72,13 +72,10 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
options, options,
}, },
}; };
const createResponse = await createUpdateSqlDatabase( const sub = params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
userContext.subscriptionId, const rg = params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
userContext.resourceGroup, const acct = params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name;
userContext.databaseAccount.name, const createResponse = await createUpdateSqlDatabase(sub, rg, acct, params.databaseId, rpPayload);
params.databaseId,
rpPayload,
);
return createResponse && (createResponse.properties.resource as DataModels.Database); return createResponse && (createResponse.properties.resource as DataModels.Database);
} }
+148 -1
View File
@@ -1,11 +1,12 @@
jest.mock("../../Utils/arm/request"); jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient"); jest.mock("../CosmosClient");
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels"; import { DatabaseAccount } from "../../Contracts/DataModels";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import { armRequest } from "../../Utils/arm/request"; import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { readDatabases } from "./readDatabases"; import { readDatabases, readDatabasesWithARM } from "./readDatabases";
describe("readDatabases", () => { describe("readDatabases", () => {
beforeAll(() => { beforeAll(() => {
@@ -42,3 +43,149 @@ describe("readDatabases", () => {
expect(client).toHaveBeenCalled(); expect(client).toHaveBeenCalled();
}); });
}); });
describe("readDatabasesWithARM (with accountOverride)", () => {
const mockDatabase = { id: "testDb", _rid: "", _self: "", _etag: "", _ts: 0 };
const mockArmResponse = { value: [{ properties: { resource: mockDatabase } }] };
beforeAll(() => {
updateUserContext({
databaseAccount: { name: "context-account" } as DatabaseAccount,
subscriptionId: "context-sub",
resourceGroup: "context-rg",
apiType: "SQL",
});
});
beforeEach(() => {
jest.clearAllMocks();
});
it("should call ARM with a path that includes the provided subscriptionId, resourceGroup, and accountName", async () => {
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
await readDatabasesWithARM({ subscriptionId: "test-sub", resourceGroup: "test-rg", accountName: "test-account" });
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({
path: expect.stringContaining("/subscriptions/test-sub/resourceGroups/test-rg/"),
}),
);
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({
path: expect.stringContaining("/databaseAccounts/test-account/sqlDatabases"),
}),
);
});
it("should use apiType from accountOverride when provided (SQL)", async () => {
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account", apiType: "SQL" });
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({ path: expect.stringContaining("/sqlDatabases") }),
);
});
it("should use apiType from accountOverride when provided (Mongo)", async () => {
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
await readDatabasesWithARM({
subscriptionId: "sub",
resourceGroup: "rg",
accountName: "account",
apiType: "Mongo",
});
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({ path: expect.stringContaining("/mongodbDatabases") }),
);
});
it("should use apiType from accountOverride when provided (Cassandra)", async () => {
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
await readDatabasesWithARM({
subscriptionId: "sub",
resourceGroup: "rg",
accountName: "account",
apiType: "Cassandra",
});
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({ path: expect.stringContaining("/cassandraKeyspaces") }),
);
});
it("should use apiType from accountOverride when provided (Gremlin)", async () => {
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
await readDatabasesWithARM({
subscriptionId: "sub",
resourceGroup: "rg",
accountName: "account",
apiType: "Gremlin",
});
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({ path: expect.stringContaining("/gremlinDatabases") }),
);
});
it("should fall back to userContext.apiType when apiType is not in accountOverride", async () => {
updateUserContext({ apiType: "Mongo" });
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" });
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({ path: expect.stringContaining("/mongodbDatabases") }),
);
updateUserContext({ apiType: "SQL" }); // restore
});
it("should throw for unsupported apiType", async () => {
await expect(
readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account", apiType: "Tables" }),
).rejects.toThrow("Unsupported default experience type: Tables");
});
it("should return mapped database resources from the response", async () => {
const db1 = { id: "db1", _rid: "r1", _self: "/dbs/db1", _etag: "", _ts: 1 };
const db2 = { id: "db2", _rid: "r2", _self: "/dbs/db2", _etag: "", _ts: 2 };
(armRequest as jest.Mock).mockResolvedValue({
value: [{ properties: { resource: db1 } }, { properties: { resource: db2 } }],
});
const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" });
expect(result).toEqual([db1, db2]);
});
it("should return an empty array when the response is null", async () => {
(armRequest as jest.Mock).mockResolvedValue(null);
const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" });
expect(result).toEqual([]);
});
it("should return an empty array when value is an empty list", async () => {
(armRequest as jest.Mock).mockResolvedValue({ value: [] });
const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" });
expect(result).toEqual([]);
});
it("should throw and propagate errors from the ARM call", async () => {
(armRequest as jest.Mock).mockRejectedValue(new Error("ARM request failed"));
await expect(
readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }),
).rejects.toThrow("ARM request failed");
});
});
+12 -5
View File
@@ -4,7 +4,7 @@ import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { FabricArtifactInfo, userContext } from "../../UserContext"; import { ApiType, FabricArtifactInfo, userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@@ -96,10 +96,17 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
return databases; return databases;
} }
async function readDatabasesWithARM(): Promise<DataModels.Database[]> { export async function readDatabasesWithARM(accountOverride?: {
subscriptionId: string;
resourceGroup: string;
accountName: string;
apiType?: ApiType;
}): Promise<DataModels.Database[]> {
let rpResponse; let rpResponse;
const { subscriptionId, resourceGroup, apiType, databaseAccount } = userContext; const subscriptionId = accountOverride?.subscriptionId ?? userContext.subscriptionId ?? "";
const accountName = databaseAccount.name; const resourceGroup = accountOverride?.resourceGroup ?? userContext.resourceGroup ?? "";
const accountName = accountOverride?.accountName ?? userContext?.databaseAccount?.name ?? "";
const apiType = accountOverride?.apiType ?? userContext.apiType;
switch (apiType) { switch (apiType) {
case "SQL": case "SQL":
@@ -118,5 +125,5 @@ async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
throw new Error(`Unsupported default experience type: ${apiType}`); throw new Error(`Unsupported default experience type: ${apiType}`);
} }
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database); return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? [];
} }
+12
View File
@@ -406,11 +406,22 @@ export interface AutoPilotOfferSettings {
targetMaxThroughput?: number; targetMaxThroughput?: number;
} }
export interface AccountOverride {
subscriptionId: string;
resourceGroup: string;
accountName: string;
capabilities: Capability[];
capacityMode?: CapacityMode;
enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean;
}
export interface CreateDatabaseParams { export interface CreateDatabaseParams {
autoPilotMaxThroughput?: number; autoPilotMaxThroughput?: number;
databaseId: string; databaseId: string;
databaseLevelThroughput?: boolean; databaseLevelThroughput?: boolean;
offerThroughput?: number; offerThroughput?: number;
targetAccountOverride?: AccountOverride;
} }
export interface CreateCollectionParamsBase { export interface CreateCollectionParamsBase {
@@ -430,6 +441,7 @@ export interface CreateCollectionParamsBase {
export interface CreateCollectionParams extends CreateCollectionParamsBase { export interface CreateCollectionParams extends CreateCollectionParamsBase {
createNewDatabase: boolean; createNewDatabase: boolean;
collectionId: string; collectionId: string;
targetAccountOverride?: AccountOverride;
} }
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase { export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
@@ -457,13 +457,13 @@ describe("CopyJobActions", () => {
jobName: "test-job", jobName: "test-job",
migrationType: "online" as any, migrationType: "online" as any,
source: { source: {
subscription: {} as any, subscriptionId: "sub-123",
account: { id: "account-1", name: "source-account" } as any, account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "sub-123", subscription: {} as any,
account: { id: "account-1", name: "target-account" } as any, account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -498,7 +498,7 @@ describe("CopyJobActions", () => {
); );
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4]; const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
expect(callArgs.properties.source.remoteAccountName).toBeUndefined(); expect(callArgs.properties.destination.remoteAccountName).toBeUndefined();
expect(mockRefreshJobList).toHaveBeenCalled(); expect(mockRefreshJobList).toHaveBeenCalled();
expect(mockOnSuccess).toHaveBeenCalled(); expect(mockOnSuccess).toHaveBeenCalled();
@@ -509,13 +509,13 @@ describe("CopyJobActions", () => {
jobName: "cross-account-job", jobName: "cross-account-job",
migrationType: "offline" as any, migrationType: "offline" as any,
source: { source: {
subscription: {} as any, subscriptionId: "sub-123",
account: { id: "account-1", name: "source-account" } as any, account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "sub-456", subscription: {} as any,
account: { id: "account-2", name: "target-account" } as any, account: { id: "account-2", name: "target-account" } as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -528,7 +528,7 @@ describe("CopyJobActions", () => {
await submitCreateCopyJob(mockState, mockOnSuccess); await submitCreateCopyJob(mockState, mockOnSuccess);
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4]; const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
expect(callArgs.properties.source.remoteAccountName).toBe("source-account"); expect(callArgs.properties.destination.remoteAccountName).toBe("target-account");
expect(mockOnSuccess).toHaveBeenCalled(); expect(mockOnSuccess).toHaveBeenCalled();
}); });
@@ -537,13 +537,13 @@ describe("CopyJobActions", () => {
jobName: "failing-job", jobName: "failing-job",
migrationType: "online" as any, migrationType: "online" as any,
source: { source: {
subscription: {} as any, subscriptionId: "sub-123",
account: { id: "account-1", name: "source-account" } as any, account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "sub-123", subscription: {} as any,
account: { id: "account-1", name: "target-account" } as any, account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -566,13 +566,13 @@ describe("CopyJobActions", () => {
jobName: "test-job", jobName: "test-job",
migrationType: "online" as any, migrationType: "online" as any,
source: { source: {
subscription: {} as any, subscriptionId: "sub-123",
account: { id: "account-1", name: "source-account" } as any, account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "sub-123", subscription: {} as any,
account: { id: "account-1", name: "target-account" } as any, account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -1,4 +1,5 @@
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers"; import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
@@ -15,7 +16,6 @@ import {
CreateJobRequest, CreateJobRequest,
DataTransferJobGetResults, DataTransferJobGetResults,
} from "../../../Utils/arm/generatedClients/dataTransferService/types"; } from "../../../Utils/arm/generatedClients/dataTransferService/types";
import ContainerCopyMessages from "../ContainerCopyMessages";
import { import {
convertTime, convertTime,
convertToCamelCase, convertToCamelCase,
@@ -35,7 +35,7 @@ export const openCreateCopyJobPanel = (explorer: Explorer) => {
const sidePanelState = useSidePanel.getState(); const sidePanelState = useSidePanel.getState();
sidePanelState.setPanelHasConsole(false); sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel( sidePanelState.openSidePanel(
ContainerCopyMessages.createCopyJobPanelTitle, t(Keys.containerCopy.createCopyJob.panelTitle),
<CreateCopyJobScreensProvider explorer={explorer} />, <CreateCopyJobScreensProvider explorer={explorer} />,
"650px", "650px",
); );
@@ -45,7 +45,7 @@ export const openCopyJobDetailsPanel = (job: CopyJobType) => {
const sidePanelState = useSidePanel.getState(); const sidePanelState = useSidePanel.getState();
sidePanelState.setPanelHasConsole(false); sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel( sidePanelState.openSidePanel(
ContainerCopyMessages.copyJobDetailsPanelTitle(job.Name), job.Name || t(Keys.containerCopy.jobDetails.panelTitleDefault),
<CopyJobDetails job={job} />, <CopyJobDetails job={job} />,
"650px", "650px",
); );
@@ -137,12 +137,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
properties: { properties: {
source: { source: {
component: "CosmosDBSql", component: "CosmosDBSql",
...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }),
databaseName: source?.databaseId, databaseName: source?.databaseId,
containerName: source?.containerId, containerName: source?.containerId,
}, },
destination: { destination: {
component: "CosmosDBSql", component: "CosmosDBSql",
...(isSameAccount ? {} : { remoteAccountName: target?.account?.name }),
databaseName: target?.databaseId, databaseName: target?.databaseId,
containerName: target?.containerId, containerName: target?.containerId,
}, },
@@ -193,7 +193,7 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
const pattern = new RegExp(`'(${statusList.join("|")})'`, "g"); const pattern = new RegExp(`'(${statusList.join("|")})'`, "g");
const normalizedErrorMessage = errorMessage.replace( const normalizedErrorMessage = errorMessage.replace(
pattern, pattern,
`'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`, `'${t(Keys.containerCopy.monitorJobs.status.inProgress)}'`,
); );
logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus"); logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus");
throw error; throw error;
@@ -5,10 +5,10 @@ import RefreshIcon from "../../../../images/refresh-cosmos.svg";
import SunIcon from "../../../../images/SunIcon.svg"; import SunIcon from "../../../../images/SunIcon.svg";
import { configContext, Platform } from "../../../ConfigContext"; import { configContext, Platform } from "../../../ConfigContext";
import { useThemeStore } from "../../../hooks/useTheme"; import { useThemeStore } from "../../../hooks/useTheme";
import { Keys, t } from "../../../Localization";
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";
import ContainerCopyMessages from "../ContainerCopyMessages";
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState"; import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes"; import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
@@ -19,15 +19,15 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
{ {
key: "createCopyJob", key: "createCopyJob",
iconSrc: AddIcon, iconSrc: AddIcon,
label: ContainerCopyMessages.createCopyJobButtonLabel, label: t(Keys.containerCopy.commandBar.createCopyJobButtonLabel),
ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel, ariaLabel: t(Keys.containerCopy.commandBar.createCopyJobButtonAriaLabel),
onClick: () => Actions.openCreateCopyJobPanel(explorer), onClick: () => Actions.openCreateCopyJobPanel(explorer),
}, },
{ {
key: "refresh", key: "refresh",
iconSrc: RefreshIcon, iconSrc: RefreshIcon,
label: ContainerCopyMessages.refreshButtonLabel, label: t(Keys.common.refresh),
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel, ariaLabel: t(Keys.containerCopy.commandBar.refreshButtonAriaLabel),
onClick: () => monitorCopyJobsRef?.refreshJobList(), onClick: () => monitorCopyJobsRef?.refreshJobList(),
}, },
{ {
@@ -48,8 +48,8 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
buttons.push({ buttons.push({
key: "feedback", key: "feedback",
iconSrc: FeedbackIcon, iconSrc: FeedbackIcon,
label: ContainerCopyMessages.feedbackButtonLabel, label: t(Keys.containerCopy.commandBar.feedbackButtonLabel),
ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel, ariaLabel: t(Keys.containerCopy.commandBar.feedbackButtonAriaLabel),
onClick: () => { onClick: () => {
explorer.openContainerCopyFeedbackBlade(); explorer.openContainerCopyFeedbackBlade();
}, },
@@ -1,193 +0,0 @@
export default {
// Copy Job Command Bar
feedbackButtonLabel: "Feedback",
feedbackButtonAriaLabel: "Provide feedback on copy jobs",
refreshButtonLabel: "Refresh",
refreshButtonAriaLabel: "Refresh copy jobs",
createCopyJobButtonLabel: "Create Copy Job",
createCopyJobButtonAriaLabel: "Create a new container copy job",
// No Copy Jobs Found
noCopyJobsTitle: "No copy jobs to show",
createCopyJobButtonText: "Create a container copy job",
// Copy Job Details
copyJobDetailsPanelTitle: (jobName: string) => jobName || "Job Details",
errorTitle: "Error Details",
selectedContainers: "Selected Containers",
// Create Copy Job Panel
createCopyJobPanelTitle: "Create copy job",
// Select Account Screen
selectAccountDescription: "Please select a source account from which to copy.",
subscriptionDropdownLabel: "Subscription",
subscriptionDropdownPlaceholder: "Select a subscription",
sourceAccountDropdownLabel: "Account",
sourceAccountDropdownPlaceholder: "Select an account",
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
selectSourceAndTargetContainersDescription:
"Please select a source container and a destination container to copy to.",
sourceContainerSubHeading: "Source container",
targetContainerSubHeading: "Destination container",
databaseDropdownLabel: "Database",
databaseDropdownPlaceholder: "Select a database",
containerDropdownLabel: "Container",
containerDropdownPlaceholder: "Select a container",
createNewContainerSubHeading: "Select the properties for your container.",
createContainerButtonLabel: "Create a new container",
createContainerHeading: "Create new container",
// Preview and Create Screen
jobNameLabel: "Job name",
sourceSubscriptionLabel: "Source subscription",
sourceAccountLabel: "Source account",
sourceDatabaseLabel: "Source database",
sourceContainerLabel: "Source container",
targetDatabaseLabel: "Destination database",
targetContainerLabel: "Destination container",
// Assign Permissions Screen
assignPermissions: {
crossAccountDescription:
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.",
intraAccountOnlineDescription: (accountName: string) =>
`Follow the steps below to enable online copy on your "${accountName}" account.`,
crossAccountConfiguration: {
title: "Cross-account container copy",
description: (sourceAccount: string, destinationAccount: string) =>
`Please follow the instruction below to grant requisite permissions to copy data from "${sourceAccount}" to "${destinationAccount}".`,
},
onlineConfiguration: {
title: "Online container copy",
description: (accountName: string) =>
`Please follow the instructions below to enable online copy on your "${accountName}" account.`,
},
},
toggleBtn: {
onText: "On",
offText: "Off",
},
popoverOverlaySpinnerLabel: "Please wait while we process your request...",
addManagedIdentity: {
title: "System-assigned managed identity enabled.",
description:
"A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.",
descriptionHrefText: "Learn more about Managed identities.",
descriptionHref: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
toggleLabel: "System assigned managed identity",
tooltip: {
content: "Learn more about",
hrefText: "Managed Identities.",
href: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
},
userAssignedIdentityTooltip: "You can select an existing user assigned identity or create a new one.",
userAssignedIdentityLabel: "You may also select a user assigned managed identity.",
createUserAssignedIdentityLink: "Create User Assigned Managed Identity",
enablementTitle: "Enable system assigned managed identity",
enablementDescription: (accountName: string) =>
accountName
? `Enable system-assigned managed identity on the ${accountName}. To confirm, click the "Yes" button.`
: "",
},
defaultManagedIdentity: {
title: "System-assigned managed identity set as default.",
description: (accountName: string) =>
`Set the system-assigned managed identity as default for "${accountName}" by switching it on.`,
tooltip: {
content: "Learn more about",
hrefText: "Default Managed Identities.",
href: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
},
popoverTitle: "System assigned managed identity set as default",
popoverDescription: (accountName: string) =>
`Assign the system-assigned managed identity as the default for "${accountName}". To confirm, click the "Yes" button. `,
},
readPermissionAssigned: {
title: "Read permissions assigned to the default identity.",
description:
"To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.",
tooltip: {
content: "Learn more about",
hrefText: "Read permissions.",
href: "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
},
popoverTitle: "Read permissions assigned to default identity.",
popoverDescription:
"Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button.",
},
pointInTimeRestore: {
title: "Point In Time Restore enabled",
description: (accessName: string) =>
`To facilitate online container copy jobs, please update your "${accessName}" backup policy from periodic to continuous backup. Enabling continuous backup is required for this functionality.`,
tooltip: {
content: "Learn more about",
hrefText: "Continuous Backup",
href: "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
},
buttonText: "Enable Point In Time Restore",
},
onlineCopyEnabled: {
title: "Online copy enabled",
description: (accountName: string) =>
`Enable online container copy by clicking the button below on your "${accountName}" account.`,
hrefText: "Learn more about online copy jobs",
href: "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
buttonText: "Enable Online Copy",
validateAllVersionsAndDeletesChangeFeedSpinnerLabel:
"Validating All versions and deletes change feed mode (preview)...",
enablingAllVersionsAndDeletesChangeFeedSpinnerLabel:
"Enabling All versions and deletes change feed mode (preview)...",
enablingOnlineCopySpinnerLabel: (accountName: string) =>
`Enabling online copy on your "${accountName}" account ...`,
},
MonitorJobs: {
Columns: {
lastUpdatedTime: "Date & time",
name: "Job name",
status: "Status",
completionPercentage: "Completion %",
duration: "Duration",
error: "Error message",
mode: "Mode",
actions: "Actions",
},
Actions: {
pause: "Pause",
resume: "Resume",
cancel: "Cancel",
complete: "Complete",
viewDetails: "View Details",
},
Status: {
Pending: "Queued",
InProgress: "Running",
Running: "Running",
Partitioning: "Running",
Paused: "Paused",
Completed: "Completed",
Failed: "Failed",
Faulted: "Failed",
Skipped: "Cancelled",
Cancelled: "Cancelled",
},
dialog: {
heading: "",
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
},
},
};
@@ -59,12 +59,6 @@ describe("CopyJobContext", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "test-subscription-id", subscriptionId: "test-subscription-id",
account: { account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account", id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
@@ -75,7 +69,13 @@ describe("CopyJobContext", () => {
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, target: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
}); });
expect(contextValue.flow).toBeNull(); expect(contextValue.flow).toBeNull();
expect(contextValue.contextError).toBeNull(); expect(contextValue.contextError).toBeNull();
@@ -598,8 +598,8 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>, </CopyJobContextProvider>,
); );
expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined(); expect(contextValue.copyJobState.source?.subscriptionId).toBe("test-subscription-id");
expect(contextValue.copyJobState.source?.account?.name).toBeUndefined(); expect(contextValue.copyJobState.source?.account?.name).toBe("test-account");
}); });
it("should initialize target with userContext values", () => { it("should initialize target with userContext values", () => {
@@ -616,11 +616,11 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>, </CopyJobContextProvider>,
); );
expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id"); expect(contextValue.copyJobState.target.subscription).toBeNull();
expect(contextValue.copyJobState.target.account.name).toBe("test-account"); expect(contextValue.copyJobState.target.account).toBeNull();
}); });
it("should initialize sourceReadAccessFromTarget as false", () => { it("should initialize sourceReadWriteAccessFromTarget as false", () => {
let contextValue: any; let contextValue: any;
render( render(
@@ -634,7 +634,7 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>, </CopyJobContextProvider>,
); );
expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false); expect(contextValue.copyJobState.sourceReadWriteAccessFromTarget).toBe(false);
}); });
it("should initialize with empty database and container ids", () => { it("should initialize with empty database and container ids", () => {
@@ -23,18 +23,18 @@ const getInitialCopyJobState = (): CopyJobContextState => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: userContext.subscriptionId || "", subscriptionId: userContext.subscriptionId || "",
account: userContext.databaseAccount || null, account: userContext.databaseAccount || null,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, target: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
}; };
}; };
@@ -2,9 +2,9 @@ import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes"; import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils"; import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import AddManagedIdentity from "./AddManagedIdentity"; import AddManagedIdentity from "./AddManagedIdentity";
@@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => {
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}; };
const mockContextValue = { const mockContextValue = {
@@ -133,16 +133,16 @@ describe("AddManagedIdentity", () => {
it("renders all required elements", () => { it("renders all required elements", () => {
renderWithContext(); renderWithContext();
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.description)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.description))).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.descriptionHrefText))).toBeInTheDocument();
expect(screen.getByRole("switch")).toBeInTheDocument(); expect(screen.getByRole("switch")).toBeInTheDocument();
}); });
it("renders description link with correct href", () => { it("renders description link with correct href", () => {
renderWithContext(); renderWithContext();
const link = screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText); const link = screen.getByText(t(Keys.containerCopy.addManagedIdentity.descriptionHrefText));
expect(link.closest("a")).toHaveAttribute("href", ContainerCopyMessages.addManagedIdentity.descriptionHref); expect(link.closest("a")).toHaveAttribute("href", t(Keys.containerCopy.addManagedIdentity.descriptionHref));
expect(link.closest("a")).toHaveAttribute("target", "_blank"); expect(link.closest("a")).toHaveAttribute("target", "_blank");
expect(link.closest("a")).toHaveAttribute("rel", "noopener noreferrer"); expect(link.closest("a")).toHaveAttribute("rel", "noopener noreferrer");
}); });
@@ -175,7 +175,7 @@ describe("AddManagedIdentity", () => {
const toggle = screen.getByRole("switch"); const toggle = screen.getByRole("switch");
fireEvent.click(toggle); fireEvent.click(toggle);
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.enablementTitle))).toBeInTheDocument();
}); });
it("hides popover when toggle is off", () => { it("hides popover when toggle is off", () => {
@@ -185,7 +185,7 @@ describe("AddManagedIdentity", () => {
fireEvent.click(toggle); fireEvent.click(toggle);
fireEvent.click(toggle); fireEvent.click(toggle);
expect(screen.queryByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).not.toBeInTheDocument(); expect(screen.queryByText(t(Keys.containerCopy.addManagedIdentity.enablementTitle))).not.toBeInTheDocument();
}); });
}); });
@@ -197,9 +197,9 @@ describe("AddManagedIdentity", () => {
}); });
it("displays correct enablement description with account name", () => { it("displays correct enablement description with account name", () => {
const expectedDescription = ContainerCopyMessages.addManagedIdentity.enablementDescription( const expectedDescription = t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
mockCopyJobState.target.account.name, accountName: mockCopyJobState.source.account.name,
); });
expect(screen.getByText(expectedDescription)).toBeInTheDocument(); expect(screen.getByText(expectedDescription)).toBeInTheDocument();
}); });
@@ -220,7 +220,7 @@ describe("AddManagedIdentity", () => {
const cancelButton = screen.getByText("Cancel"); const cancelButton = screen.getByText("Cancel");
fireEvent.click(cancelButton); fireEvent.click(cancelButton);
expect(screen.queryByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).not.toBeInTheDocument(); expect(screen.queryByText(t(Keys.containerCopy.addManagedIdentity.enablementTitle))).not.toBeInTheDocument();
const toggle = screen.getByRole("switch"); const toggle = screen.getByRole("switch");
expect(toggle).not.toBeChecked(); expect(toggle).not.toBeChecked();
@@ -1,7 +1,7 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react"; import { Link, Stack, Text, Toggle } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils"; import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import InfoTooltip from "../Components/InfoTooltip"; import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer"; import PopoverMessage from "../Components/PopoverContainer";
@@ -11,14 +11,14 @@ import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ( const managedIdentityTooltip = (
<Text> <Text>
{ContainerCopyMessages.addManagedIdentity.tooltip.content} &nbsp; {t(Keys.containerCopy.addManagedIdentity.tooltipContent)} &nbsp;
<Link <Link
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.addManagedIdentity.tooltip.href} href={t(Keys.containerCopy.addManagedIdentity.tooltipHref)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText} {t(Keys.containerCopy.addManagedIdentity.tooltipHrefText)}
</Link> </Link>
</Text> </Text>
); );
@@ -32,9 +32,9 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
return ( return (
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Text className="themeText"> <Text className="themeText">
{ContainerCopyMessages.addManagedIdentity.description}&ensp; {t(Keys.containerCopy.addManagedIdentity.description)}&ensp;
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer"> <Link href={t(Keys.containerCopy.addManagedIdentity.descriptionHref)} target="_blank" rel="noopener noreferrer">
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText} {t(Keys.containerCopy.addManagedIdentity.descriptionHrefText)}
</Link>{" "} </Link>{" "}
&nbsp; &nbsp;
<InfoTooltip content={managedIdentityTooltip} /> <InfoTooltip content={managedIdentityTooltip} />
@@ -42,18 +42,20 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<Toggle <Toggle
data-test="btn-toggle" data-test="btn-toggle"
checked={systemAssigned} checked={systemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={t(Keys.common.on)}
offText={ContainerCopyMessages.toggleBtn.offText} offText={t(Keys.common.off)}
onChange={onToggle} onChange={onToggle}
/> />
<PopoverMessage <PopoverMessage
isLoading={loading} isLoading={loading}
visible={systemAssigned} visible={systemAssigned}
title={ContainerCopyMessages.addManagedIdentity.enablementTitle} title={t(Keys.containerCopy.addManagedIdentity.enablementTitle)}
onCancel={() => onToggle(null, false)} onCancel={() => onToggle(null, false)}
onPrimary={handleAddSystemIdentity} onPrimary={handleAddSystemIdentity}
> >
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)} {t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
accountName: copyJobState.source?.account?.name,
})}
</PopoverMessage> </PopoverMessage>
</Stack> </Stack>
); );
@@ -1,10 +1,10 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { Keys, t } from "Localization";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes"; import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes";
import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity"; import AddReadWritePermissionToDefaultIdentity from "./AddReadWritePermissionToDefaultIdentity";
jest.mock("../../../../../Common/Logger", () => ({ jest.mock("../../../../../Common/Logger", () => ({
logError: jest.fn(), logError: jest.fn(),
@@ -73,7 +73,7 @@ import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUti
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import useToggle from "./hooks/useToggle"; import useToggle from "./hooks/useToggle";
describe("AddReadPermissionToDefaultIdentity Component", () => { describe("AddReadWritePermissionToDefaultIdentity Component", () => {
const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>; const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>; const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>;
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction< const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
@@ -86,7 +86,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
jobName: "test-job", jobName: "test-job",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: { subscriptionId: "source-sub-id" } as Subscription, subscriptionId: "source-sub-id",
account: { account: {
id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account", id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
name: "source-account", name: "source-account",
@@ -96,12 +96,16 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
properties: { properties: {
documentEndpoint: "https://source-account.documents.azure.com:443/", documentEndpoint: "https://source-account.documents.azure.com:443/",
}, },
identity: {
principalId: "source-principal-id",
type: "SystemAssigned",
},
}, },
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "target-sub-id", subscription: { subscriptionId: "target-sub-id" } as Subscription,
account: { account: {
id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account", id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
name: "target-account", name: "target-account",
@@ -119,7 +123,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}, },
setCopyJobState: jest.fn(), setCopyJobState: jest.fn(),
setContextError: jest.fn(), setContextError: jest.fn(),
@@ -133,7 +137,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
const renderComponent = (contextValue = mockContextValue) => { const renderComponent = (contextValue = mockContextValue) => {
return render( return render(
<CopyJobContext.Provider value={contextValue}> <CopyJobContext.Provider value={contextValue}>
<AddReadPermissionToDefaultIdentity /> <AddReadWritePermissionToDefaultIdentity />
</CopyJobContext.Provider>, </CopyJobContext.Provider>,
); );
}; };
@@ -164,12 +168,12 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it("should render correctly when sourceReadAccessFromTarget is true", () => { it("should render correctly when sourceReadWriteAccessFromTarget is true", () => {
const contextWithAccess = { const contextWithAccess = {
...mockContextValue, ...mockContextValue,
copyJobState: { copyJobState: {
...mockContextValue.copyJobState, ...mockContextValue.copyJobState,
sourceReadAccessFromTarget: true, sourceReadWriteAccessFromTarget: true,
}, },
}; };
const { container } = renderComponent(contextWithAccess); const { container } = renderComponent(contextWithAccess);
@@ -180,7 +184,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
describe("Component Structure", () => { describe("Component Structure", () => {
it("should display the description text", () => { it("should display the description text", () => {
renderComponent(); renderComponent();
expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.readWritePermissionAssigned.description))).toBeInTheDocument();
}); });
it("should display the info tooltip", () => { it("should display the info tooltip", () => {
@@ -212,10 +216,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(screen.getByTestId("popover-message")).toBeInTheDocument(); expect(screen.getByTestId("popover-message")).toBeInTheDocument();
expect(screen.getByTestId("popover-title")).toHaveTextContent( expect(screen.getByTestId("popover-title")).toHaveTextContent(
ContainerCopyMessages.readPermissionAssigned.popoverTitle, t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle),
); );
expect(screen.getByTestId("popover-content")).toHaveTextContent( expect(screen.getByTestId("popover-content")).toHaveTextContent(
ContainerCopyMessages.readPermissionAssigned.popoverDescription, t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription),
); );
}); });
@@ -243,11 +247,11 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(mockOnToggle).toHaveBeenCalledWith(null, false); expect(mockOnToggle).toHaveBeenCalledWith(null, false);
}); });
it("should call handleAddReadPermission when primary button is clicked", async () => { it("should call handleAddReadWritePermission when primary button is clicked", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
@@ -258,22 +262,22 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => { await waitFor(() => {
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith( expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith(
"/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account", "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
); );
}); });
}); });
}); });
describe("handleAddReadPermission Function", () => { describe("handleAddReadWritePermission Function", () => {
beforeEach(() => { beforeEach(() => {
mockUseToggle.mockReturnValue([true, jest.fn()]); mockUseToggle.mockReturnValue([true, jest.fn()]);
}); });
it("should successfully assign role and update context", async () => { it("should successfully assign role and update context", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
@@ -284,10 +288,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => { await waitFor(() => {
expect(mockAssignRole).toHaveBeenCalledWith( expect(mockAssignRole).toHaveBeenCalledWith(
"source-sub-id", "target-sub-id",
"source-rg", "target-rg",
"source-account", "target-account",
"target-principal-id", "source-principal-id",
); );
}); });
@@ -298,9 +302,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
it("should handle error when assignRole fails", async () => { it("should handle error when assignRole fails", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockRejectedValue(new Error("Permission denied")); mockAssignRole.mockRejectedValue(new Error("Permission denied"));
@@ -312,7 +316,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => { await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith( expect(mockLogError).toHaveBeenCalledWith(
"Permission denied", "Permission denied",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission", "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
); );
}); });
@@ -323,9 +327,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
it("should handle error without message", async () => { it("should handle error without message", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockRejectedValue({}); mockAssignRole.mockRejectedValue({});
@@ -336,23 +340,23 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => { await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith( expect(mockLogError).toHaveBeenCalledWith(
"Error assigning read permission to default identity. Please try again later.", "Error assigning read-write permission to default identity. Please try again later.",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission", "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
); );
}); });
await waitFor(() => { await waitFor(() => {
expect(mockContextValue.setContextError).toHaveBeenCalledWith( expect(mockContextValue.setContextError).toHaveBeenCalledWith(
"Error assigning read permission to default identity. Please try again later.", "Error assigning read-write permission to default identity. Please try again later.",
); );
}); });
}); });
it("should show loading state during role assignment", async () => { it("should show loading state during role assignment", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockImplementation( mockAssignRole.mockImplementation(
@@ -371,9 +375,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
it.skip("should not assign role when assignRole returns falsy", async () => { it.skip("should not assign role when assignRole returns falsy", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue(null); mockAssignRole.mockResolvedValue(null);
@@ -431,10 +435,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
...mockContextValue, ...mockContextValue,
copyJobState: { copyJobState: {
...mockContextValue.copyJobState, ...mockContextValue.copyJobState,
target: { source: {
...mockContextValue.copyJobState.target, ...mockContextValue.copyJobState.source,
account: { account: {
...mockContextValue.copyJobState.target.account!, ...mockContextValue.copyJobState.source.account!,
identity: { identity: {
principalId: "", principalId: "",
type: "SystemAssigned", type: "SystemAssigned",
@@ -446,9 +450,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
mockUseToggle.mockReturnValue([true, jest.fn()]); mockUseToggle.mockReturnValue([true, jest.fn()]);
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
@@ -458,7 +462,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
fireEvent.click(primaryButton); fireEvent.click(primaryButton);
await waitFor(() => { await waitFor(() => {
expect(mockAssignRole).toHaveBeenCalledWith("source-sub-id", "source-rg", "source-account", ""); expect(mockAssignRole).toHaveBeenCalledWith("target-sub-id", "target-rg", "target-account", "");
}); });
}); });
}); });
@@ -476,9 +480,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
mockUseToggle.mockReturnValue([true, jest.fn()]); mockUseToggle.mockReturnValue([true, jest.fn()]);
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
@@ -496,7 +500,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(updatedState).toEqual({ expect(updatedState).toEqual({
...mockContextValue.copyJobState, ...mockContextValue.copyJobState,
sourceReadAccessFromTarget: true, sourceReadWriteAccessFromTarget: true,
}); });
}); });
}); });
@@ -1,8 +1,8 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react"; import { Link, Stack, Text, Toggle } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import { assignRole } from "../../../../../Utils/arm/RbacUtils"; import { assignRole } from "../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import InfoTooltip from "../Components/InfoTooltip"; import InfoTooltip from "../Components/InfoTooltip";
@@ -12,51 +12,54 @@ import useToggle from "./hooks/useToggle";
const TooltipContent = ( const TooltipContent = (
<Text> <Text>
{ContainerCopyMessages.readPermissionAssigned.tooltip.content} &nbsp; {t(Keys.containerCopy.readWritePermissionAssigned.tooltipContent)} &nbsp;
<Link <Link
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href} href={t(Keys.containerCopy.readWritePermissionAssigned.tooltipHref)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText} {t(Keys.containerCopy.readWritePermissionAssigned.tooltipHrefText)}
</Link> </Link>
</Text> </Text>
); );
type AddReadPermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIdentityProps> = () => { type AddReadWritePermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionToDefaultIdentityProps> = () => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext(); const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
const [readPermissionAssigned, onToggle] = useToggle(false); const [readWritePermissionAssigned, onToggle] = useToggle(copyJobState.sourceReadWriteAccessFromTarget ?? false);
const handleAddReadPermission = async () => { const handleAddReadWritePermission = async () => {
const { source, target } = copyJobState; const { source, target } = copyJobState;
const selectedSourceAccount = source?.account; const selectedTargetAccount = target?.account;
try { try {
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: targetSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: targetResourceGroup,
accountName: sourceAccountName, accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
setLoading(true); setLoading(true);
const assignedRole = await assignRole( const assignedRole = await assignRole(
sourceSubscriptionId, targetSubscriptionId,
sourceResourceGroup, targetResourceGroup,
sourceAccountName, targetAccountName,
target?.account?.identity?.principalId ?? "", source?.account?.identity?.principalId ?? "",
); );
if (assignedRole) { if (assignedRole) {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
...prevState, ...prevState,
sourceReadAccessFromTarget: true, sourceReadWriteAccessFromTarget: true,
})); }));
} }
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error.message || "Error assigning read permission to default identity. Please try again later."; error.message || "Error assigning read-write permission to default identity. Please try again later.";
logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission"); logError(errorMessage, "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission");
setContextError(errorMessage); setContextError(errorMessage);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -66,14 +69,14 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
return ( return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Text className="toggle-label"> <Text className="toggle-label">
{ContainerCopyMessages.readPermissionAssigned.description}&ensp; {t(Keys.containerCopy.readWritePermissionAssigned.description)}&ensp;
<InfoTooltip content={TooltipContent} /> <InfoTooltip content={TooltipContent} />
</Text> </Text>
<Toggle <Toggle
data-test="btn-toggle" data-test="btn-toggle"
checked={readPermissionAssigned} checked={readWritePermissionAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={t(Keys.common.on)}
offText={ContainerCopyMessages.toggleBtn.offText} offText={t(Keys.common.off)}
onChange={onToggle} onChange={onToggle}
inlineLabel inlineLabel
styles={{ styles={{
@@ -83,15 +86,15 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
/> />
<PopoverMessage <PopoverMessage
isLoading={loading} isLoading={loading}
visible={readPermissionAssigned} visible={readWritePermissionAssigned}
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle} title={t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle)}
onCancel={() => onToggle(null, false)} onCancel={() => onToggle(null, false)}
onPrimary={handleAddReadPermission} onPrimary={handleAddReadWritePermission}
> >
{ContainerCopyMessages.readPermissionAssigned.popoverDescription} {t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription)}
</PopoverMessage> </PopoverMessage>
</Stack> </Stack>
); );
}; };
export default AddReadPermissionToDefaultIdentity; export default AddReadWritePermissionToDefaultIdentity;
@@ -1,7 +1,7 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { render, RenderResult } from "@testing-library/react"; import { render, RenderResult } from "@testing-library/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { Keys, t } from "Localization";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes"; import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes";
@@ -43,12 +43,12 @@ jest.mock("./AddManagedIdentity", () => {
return MockAddManagedIdentity; return MockAddManagedIdentity;
}); });
jest.mock("./AddReadPermissionToDefaultIdentity", () => { jest.mock("./AddReadWritePermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => { const MockAddReadWritePermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">Add Read Permission Component</div>; return <div data-testid="add-read-write-permission">Add Read-Write Permission Component</div>;
}; };
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity"; MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity; return MockAddReadWritePermissionToDefaultIdentity;
}); });
jest.mock("./DefaultManagedIdentity", () => { jest.mock("./DefaultManagedIdentity", () => {
@@ -85,18 +85,18 @@ describe("AssignPermissions Component", () => {
jobName: "test-job", jobName: "test-job",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: { subscriptionId: "source-sub" } as any, subscriptionId: "source-sub",
account: { id: "source-account", name: "Source Account" } as any, account: { id: "source-account", name: "Source Account" } as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "target-sub", subscription: { subscriptionId: "target-sub" } as any,
account: { id: "target-account", name: "Target Account" } as any, account: { id: "target-account", name: "Target Account" } as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
...overrides, ...overrides,
}); });
@@ -154,7 +154,7 @@ describe("AssignPermissions Component", () => {
const copyJobState = createMockCopyJobState(); const copyJobState = createMockCopyJobState();
const { getByText } = renderWithContext(copyJobState); const { getByText } = renderWithContext(copyJobState);
expect(getByText(ContainerCopyMessages.assignPermissions.crossAccountDescription)).toBeInTheDocument(); expect(getByText(t(Keys.containerCopy.assignPermissions.crossAccountDescription))).toBeInTheDocument();
}); });
it("should display intra account description for same accounts with online migration", async () => { it("should display intra account description for same accounts with online migration", async () => {
@@ -164,13 +164,13 @@ describe("AssignPermissions Component", () => {
const copyJobState = createMockCopyJobState({ const copyJobState = createMockCopyJobState({
migrationType: CopyJobMigrationType.Online, migrationType: CopyJobMigrationType.Online,
source: { source: {
subscription: { subscriptionId: "same-sub" } as any, subscriptionId: "same-sub",
account: { id: "same-account", name: "Same Account" } as any, account: { id: "same-account", name: "Same Account" } as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "same-sub", subscription: { subscriptionId: "same-sub" } as any,
account: { id: "same-account", name: "Same Account" } as any, account: { id: "same-account", name: "Same Account" } as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -179,7 +179,9 @@ describe("AssignPermissions Component", () => {
const { getByText } = renderWithContext(copyJobState); const { getByText } = renderWithContext(copyJobState);
expect( expect(
getByText(ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription("Same Account")), getByText(
t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, { accountName: "Same Account" }),
),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });
@@ -201,7 +203,7 @@ describe("AssignPermissions Component", () => {
completed: true, completed: true,
}, },
{ {
id: "readPermissionAssigned", id: "readWritePermissionAssigned",
title: "Read Permission Assigned", title: "Read Permission Assigned",
Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>, Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>,
disabled: false, disabled: false,
@@ -347,7 +349,7 @@ describe("AssignPermissions Component", () => {
it("should handle missing account names", () => { it("should handle missing account names", () => {
const copyJobState = createMockCopyJobState({ const copyJobState = createMockCopyJobState({
source: { source: {
subscription: { subscriptionId: "source-sub" } as any, subscriptionId: "source-sub",
account: { id: "source-account" } as any, account: { id: "source-account" } as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
@@ -1,10 +1,10 @@
import { Image, Stack, Text } from "@fluentui/react"; import { Image, Stack, Text } from "@fluentui/react";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components"; import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
import { Keys, t } from "Localization";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg"; import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
import WarningIcon from "../../../../../../images/warning.svg"; import WarningIcon from "../../../../../../images/warning.svg";
import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree"; import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { isIntraAccountCopy } from "../../../CopyJobUtils"; import { isIntraAccountCopy } from "../../../CopyJobUtils";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
@@ -106,11 +106,11 @@ const AssignPermissions = () => {
tokens={{ childrenGap: 20 }} tokens={{ childrenGap: 20 }}
> >
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online {isSameAccount && copyJobState?.migrationType === CopyJobMigrationType.Online
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription( ? t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, {
copyJobState?.source?.account?.name || "", accountName: copyJobState?.source?.account?.name || "",
) })
: ContainerCopyMessages.assignPermissions.crossAccountDescription} : t(Keys.containerCopy.assignPermissions.crossAccountDescription)}
</Text> </Text>
{totalSectionsCount === 0 ? ( {totalSectionsCount === 0 ? (
@@ -1,8 +1,8 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils"; import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import DefaultManagedIdentity from "./DefaultManagedIdentity"; import DefaultManagedIdentity from "./DefaultManagedIdentity";
@@ -69,6 +69,12 @@ const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
describe("DefaultManagedIdentity", () => { describe("DefaultManagedIdentity", () => {
const mockCopyJobContextValue = { const mockCopyJobContextValue = {
copyJobState: { copyJobState: {
source: {
account: {
name: "test-cosmos-account",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos-account",
},
},
target: { target: {
account: { account: {
name: "test-cosmos-account", name: "test-cosmos-account",
@@ -166,7 +172,7 @@ describe("DefaultManagedIdentity", () => {
expect(popover).toBeInTheDocument(); expect(popover).toBeInTheDocument();
const title = screen.getByTestId("popover-title"); const title = screen.getByTestId("popover-title");
expect(title).toHaveTextContent(ContainerCopyMessages.defaultManagedIdentity.popoverTitle); expect(title).toHaveTextContent(t(Keys.containerCopy.defaultManagedIdentity.popoverTitle));
const content = screen.getByTestId("popover-content"); const content = screen.getByTestId("popover-content");
expect(content).toHaveTextContent( expect(content).toHaveTextContent(
@@ -260,6 +266,12 @@ describe("DefaultManagedIdentity", () => {
const contextValueWithoutAccount = { const contextValueWithoutAccount = {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
source: {
account: {
name: "",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/",
},
},
target: { target: {
account: { account: {
name: "", name: "",
@@ -277,6 +289,9 @@ describe("DefaultManagedIdentity", () => {
const contextValueWithNullAccount = { const contextValueWithNullAccount = {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
source: {
account: null as DatabaseAccount | null,
},
target: { target: {
account: null as DatabaseAccount | null, account: null as DatabaseAccount | null,
}, },
@@ -339,8 +354,8 @@ describe("DefaultManagedIdentity", () => {
it("should display correct toggle button text", () => { it("should display correct toggle button text", () => {
renderComponent(); renderComponent();
const onText = screen.queryByText(ContainerCopyMessages.toggleBtn.onText); const onText = screen.queryByText(t(Keys.common.on));
const offText = screen.queryByText(ContainerCopyMessages.toggleBtn.offText); const offText = screen.queryByText(t(Keys.common.off));
expect(onText || offText).toBeTruthy(); expect(onText || offText).toBeTruthy();
}); });
@@ -348,7 +363,7 @@ describe("DefaultManagedIdentity", () => {
it("should display correct link text in tooltip", () => { it("should display correct link text in tooltip", () => {
renderComponent(); renderComponent();
const linkText = screen.getByText(ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText); const linkText = screen.getByText(t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText));
expect(linkText).toBeInTheDocument(); expect(linkText).toBeInTheDocument();
}); });
}); });
@@ -1,7 +1,7 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react"; import { Link, Stack, Text, Toggle } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils"; import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import InfoTooltip from "../Components/InfoTooltip"; import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer"; import PopoverMessage from "../Components/PopoverContainer";
@@ -11,14 +11,14 @@ import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ( const managedIdentityTooltip = (
<Text> <Text>
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content} &nbsp; {t(Keys.containerCopy.defaultManagedIdentity.tooltipContent)} &nbsp;
<Link <Link
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href} href={t(Keys.containerCopy.defaultManagedIdentity.tooltipHref)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText} {t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText)}
</Link> </Link>
</Text> </Text>
); );
@@ -32,14 +32,17 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
return ( return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label"> <div className="toggle-label">
{ContainerCopyMessages.defaultManagedIdentity.description(copyJobState?.target?.account?.name)} &nbsp; {t(Keys.containerCopy.defaultManagedIdentity.description, {
accountName: copyJobState?.source?.account?.name,
})}{" "}
&nbsp;
<InfoTooltip content={managedIdentityTooltip} /> <InfoTooltip content={managedIdentityTooltip} />
</div> </div>
<Toggle <Toggle
data-test="btn-toggle" data-test="btn-toggle"
checked={defaultSystemAssigned} checked={defaultSystemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={t(Keys.common.on)}
offText={ContainerCopyMessages.toggleBtn.offText} offText={t(Keys.common.off)}
onChange={onToggle} onChange={onToggle}
inlineLabel inlineLabel
styles={{ styles={{
@@ -50,11 +53,13 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<PopoverMessage <PopoverMessage
isLoading={loading} isLoading={loading}
visible={defaultSystemAssigned} visible={defaultSystemAssigned}
title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle} title={t(Keys.containerCopy.defaultManagedIdentity.popoverTitle)}
onCancel={() => onToggle(null, false)} onCancel={() => onToggle(null, false)}
onPrimary={handleAddSystemIdentity} onPrimary={handleAddSystemIdentity}
> >
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription(copyJobState?.target?.account?.name)} {t(Keys.containerCopy.defaultManagedIdentity.popoverDescription, {
accountName: copyJobState?.source?.account?.name,
})}
</PopoverMessage> </PopoverMessage>
</Stack> </Stack>
); );
@@ -2,12 +2,12 @@ import "@testing-library/jest-dom";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes"; import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { CapabilityNames } from "../../../../../Common/Constants"; import { CapabilityNames } from "../../../../../Common/Constants";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import OnlineCopyEnabled from "./OnlineCopyEnabled"; import OnlineCopyEnabled from "./OnlineCopyEnabled";
@@ -97,7 +97,9 @@ describe("OnlineCopyEnabled", () => {
it("should render the description with account name", () => { it("should render the description with account name", () => {
renderComponent(); renderComponent();
const description = screen.getByText(ContainerCopyMessages.onlineCopyEnabled.description("test-account")); const description = screen.getByText(
t(Keys.containerCopy.onlineCopyEnabled.description, { accountName: "test-account" }),
);
expect(description).toBeInTheDocument(); expect(description).toBeInTheDocument();
}); });
@@ -105,10 +107,10 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const link = screen.getByRole("link", { const link = screen.getByRole("link", {
name: ContainerCopyMessages.onlineCopyEnabled.hrefText, name: t(Keys.containerCopy.onlineCopyEnabled.hrefText),
}); });
expect(link).toBeInTheDocument(); expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", ContainerCopyMessages.onlineCopyEnabled.href); expect(link).toHaveAttribute("href", t(Keys.containerCopy.onlineCopyEnabled.href));
expect(link).toHaveAttribute("target", "_blank"); expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer"); expect(link).toHaveAttribute("rel", "noopener noreferrer");
}); });
@@ -117,7 +119,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
expect(button).not.toBeDisabled(); expect(button).not.toBeDisabled();
@@ -134,7 +136,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const refreshButton = screen.queryByRole("button", { const refreshButton = screen.queryByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel, name: t(Keys.common.refresh),
}); });
expect(refreshButton).not.toBeInTheDocument(); expect(refreshButton).not.toBeInTheDocument();
}); });
@@ -167,7 +169,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -222,7 +224,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -246,7 +248,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -259,7 +261,9 @@ describe("OnlineCopyEnabled", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel("test-account")), screen.getByText(
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { accountName: "test-account" }),
),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });
@@ -272,7 +276,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -306,7 +310,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -318,7 +322,7 @@ describe("OnlineCopyEnabled", () => {
}); });
const refreshButton = screen.getByRole("button", { const refreshButton = screen.getByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel, name: t(Keys.common.refresh),
}); });
await act(async () => { await act(async () => {
@@ -349,7 +353,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -379,7 +383,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -401,7 +405,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -418,7 +422,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -436,7 +440,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -450,7 +454,7 @@ describe("OnlineCopyEnabled", () => {
mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {})); mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {}));
const refreshButton = screen.getByRole("button", { const refreshButton = screen.getByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel, name: t(Keys.common.refresh),
}); });
await act(async () => { await act(async () => {
@@ -536,7 +540,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(contextWithNoCapabilities); renderComponent(contextWithNoCapabilities);
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
expect(enableButton).toBeInTheDocument(); expect(enableButton).toBeInTheDocument();
}); });
@@ -547,7 +551,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
}); });
@@ -1,12 +1,12 @@
import { Link, PrimaryButton, Stack } from "@fluentui/react"; import { Link, PrimaryButton, Stack } from "@fluentui/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { CapabilityNames } from "../../../../../Common/Constants"; import { CapabilityNames } from "../../../../../Common/Constants";
import LoadingOverlay from "../../../../../Common/LoadingOverlay"; import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import { AccountValidatorFn } from "../../../Types/CopyJobTypes"; import { AccountValidatorFn } from "../../../Types/CopyJobTypes";
@@ -76,21 +76,25 @@ const OnlineCopyEnabled: React.FC = () => {
setShowRefreshButton(false); setShowRefreshButton(false);
try { try {
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel); setLoaderMessage(t(Keys.containerCopy.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel));
const sourAccountBeforeUpdate = await fetchDatabaseAccount( const sourAccountBeforeUpdate = await fetchDatabaseAccount(
sourceSubscriptionId, sourceSubscriptionId,
sourceResourceGroup, sourceResourceGroup,
sourceAccountName, sourceAccountName,
); );
if (!sourAccountBeforeUpdate?.properties.enableAllVersionsAndDeletesChangeFeed) { if (!sourAccountBeforeUpdate?.properties.enableAllVersionsAndDeletesChangeFeed) {
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel); setLoaderMessage(t(Keys.containerCopy.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel));
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
properties: { properties: {
enableAllVersionsAndDeletesChangeFeed: true, enableAllVersionsAndDeletesChangeFeed: true,
}, },
}); });
} }
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel(sourceAccountName)); setLoaderMessage(
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, {
accountName: sourceAccountName,
}),
);
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
properties: { properties: {
capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }], capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }],
@@ -132,16 +136,16 @@ const OnlineCopyEnabled: React.FC = () => {
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<LoadingOverlay isLoading={loading} label={loaderMessage} /> <LoadingOverlay isLoading={loading} label={loaderMessage} />
<Stack.Item className="info-message"> <Stack.Item className="info-message">
{ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")}&ensp; {t(Keys.containerCopy.onlineCopyEnabled.description, { accountName: source?.account?.name || "" })}&ensp;
<Link href={ContainerCopyMessages.onlineCopyEnabled.href} target="_blank" rel="noopener noreferrer"> <Link href={t(Keys.containerCopy.onlineCopyEnabled.href)} target="_blank" rel="noopener noreferrer">
{ContainerCopyMessages.onlineCopyEnabled.hrefText} {t(Keys.containerCopy.onlineCopyEnabled.hrefText)}
</Link> </Link>
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>
{showRefreshButton ? ( {showRefreshButton ? (
<PrimaryButton <PrimaryButton
className="fullWidth" className="fullWidth"
text={ContainerCopyMessages.refreshButtonLabel} text={t(Keys.common.refresh)}
iconProps={{ iconName: "Refresh" }} iconProps={{ iconName: "Refresh" }}
onClick={handleRefresh} onClick={handleRefresh}
disabled={loading} disabled={loading}
@@ -149,7 +153,7 @@ const OnlineCopyEnabled: React.FC = () => {
) : ( ) : (
<PrimaryButton <PrimaryButton
className="fullWidth" className="fullWidth"
text={loading ? "" : ContainerCopyMessages.onlineCopyEnabled.buttonText} text={loading ? "" : t(Keys.containerCopy.onlineCopyEnabled.buttonText)}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})} {...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
disabled={loading} disabled={loading}
onClick={handleOnlineCopyEnable} onClick={handleOnlineCopyEnable}
@@ -50,18 +50,18 @@ describe("PointInTimeRestore", () => {
jobName: "test-job", jobName: "test-job",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" }, subscriptionId: "test-sub",
account: mockSourceAccount, account: mockSourceAccount,
databaseId: "test-db", databaseId: "test-db",
containerId: "test-container", containerId: "test-container",
}, },
target: { target: {
subscriptionId: "test-sub", subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
account: mockSourceAccount, account: mockSourceAccount,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
} as CopyJobContextState; } as CopyJobContextState;
const mockSetCopyJobState = jest.fn(); const mockSetCopyJobState = jest.fn();
@@ -1,10 +1,10 @@
import { Link, PrimaryButton, Stack, Text } from "@fluentui/react"; import { Link, PrimaryButton, Stack, Text } from "@fluentui/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import LoadingOverlay from "../../../../../Common/LoadingOverlay"; import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import { AccountValidatorFn } from "../../../Types/CopyJobTypes"; import { AccountValidatorFn } from "../../../Types/CopyJobTypes";
@@ -12,14 +12,14 @@ import InfoTooltip from "../Components/InfoTooltip";
const tooltipContent = ( const tooltipContent = (
<Text> <Text>
{ContainerCopyMessages.pointInTimeRestore.tooltip.content} &nbsp; {t(Keys.containerCopy.pointInTimeRestore.tooltipContent)} &nbsp;
<Link <Link
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.pointInTimeRestore.tooltip.href} href={t(Keys.containerCopy.pointInTimeRestore.tooltipHref)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText} {t(Keys.containerCopy.pointInTimeRestore.tooltipHrefText)}
</Link> </Link>
</Text> </Text>
); );
@@ -119,9 +119,9 @@ const PointInTimeRestore: React.FC = () => {
return ( return (
<Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<LoadingOverlay isLoading={loading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} /> <LoadingOverlay isLoading={loading} label={t(Keys.containerCopy.popoverOverlaySpinnerLabel)} />
<Stack.Item className="toggle-label"> <Stack.Item className="toggle-label">
{ContainerCopyMessages.pointInTimeRestore.description(source.account?.name ?? "")} {t(Keys.containerCopy.pointInTimeRestore.description, { accessName: source.account?.name ?? "" })}
{tooltipContent && ( {tooltipContent && (
<> <>
{" "} {" "}
@@ -134,7 +134,7 @@ const PointInTimeRestore: React.FC = () => {
<PrimaryButton <PrimaryButton
data-test="pointInTimeRestore:RefreshBtn" data-test="pointInTimeRestore:RefreshBtn"
className="fullWidth" className="fullWidth"
text={ContainerCopyMessages.refreshButtonLabel} text={t(Keys.common.refresh)}
iconProps={{ iconName: "Refresh" }} iconProps={{ iconName: "Refresh" }}
onClick={handleRefresh} onClick={handleRefresh}
/> />
@@ -142,7 +142,7 @@ const PointInTimeRestore: React.FC = () => {
<PrimaryButton <PrimaryButton
data-test="pointInTimeRestore:PrimaryBtn" data-test="pointInTimeRestore:PrimaryBtn"
className="fullWidth" className="fullWidth"
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText} text={loading ? "" : t(Keys.containerCopy.pointInTimeRestore.buttonText)}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})} {...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
disabled={loading} disabled={loading}
onClick={openWindowAndMonitor} onClick={openWindowAndMonitor}
@@ -7,7 +7,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
<span <span
class="themeText css-110" class="themeText css-110"
> >
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code. A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.
<a <a
class="ms-Link root-111" class="ms-Link root-111"
@@ -95,7 +95,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
<span <span
class="themeText css-110" class="themeText css-110"
> >
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code. A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.
<a <a
class="ms-Link root-111" class="ms-Link root-111"
@@ -204,7 +204,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
<span <span
class="themeText 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 source-account-name. To confirm, click the "Yes" button.
</span> </span>
<div <div
class="ms-Stack css-125" class="ms-Stack css-125"
@@ -267,7 +267,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
<span <span
class="themeText css-110" class="themeText css-110"
> >
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code. A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.
<a <a
class="ms-Link root-111" class="ms-Link root-111"
@@ -359,7 +359,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
<span <span
class="themeText 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 source-account-name. To confirm, click the "Yes" button.
</span> </span>
<div <div
class="ms-Stack css-125" class="ms-Stack css-125"
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -8,7 +8,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -24,7 +24,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -63,7 +63,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -71,7 +71,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -87,7 +87,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -126,7 +126,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when sourceReadAccessFromTarget is true 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when sourceReadWriteAccessFromTarget is true 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -134,7 +134,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -150,7 +150,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -189,7 +189,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -197,7 +197,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -213,7 +213,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -255,12 +255,12 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<div <div
data-testid="popover-title" data-testid="popover-title"
> >
Read permissions assigned to default identity. Assign read-write permissions to default identity.
</div> </div>
<div <div
data-testid="popover-content" data-testid="popover-content"
> >
Assign read permissions of the source account to the default identity of the destination account. To confirm click the Yes button. Assign read-write permissions on the destination account to the default identity of the source account. To confirm, click the "Yes" button.
</div> </div>
<button <button
data-testid="popover-cancel" data-testid="popover-cancel"
@@ -277,7 +277,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -285,7 +285,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -301,7 +301,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -340,7 +340,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -348,7 +348,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -364,7 +364,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -9,7 +9,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
class="ms-Stack css-111" class="ms-Stack css-111"
@@ -212,7 +212,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
class="ms-Stack css-111" class="ms-Stack css-111"
@@ -618,7 +618,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
class="ms-Stack css-111" class="ms-Stack css-111"
@@ -1153,7 +1153,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
class="ms-Stack css-111" class="ms-Stack css-111"
@@ -1307,7 +1307,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
data-testid="shimmer-tree" data-testid="shimmer-tree"
@@ -1329,7 +1329,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
data-testid="shimmer-tree" data-testid="shimmer-tree"
@@ -9,7 +9,8 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "" by switching it on. Set the system-assigned managed identity as default for "" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -71,8 +72,9 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
<div <div
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "undefined" by switching it on. Set the system-assigned managed identity as default for "" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -135,7 +137,8 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on. Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -227,7 +230,8 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on. Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -290,7 +294,8 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on. Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -26,18 +26,18 @@ const useManagedIdentity = (
const handleAddSystemIdentity = useCallback(async (): Promise<void> => { const handleAddSystemIdentity = useCallback(async (): Promise<void> => {
try { try {
setLoading(true); setLoading(true);
const selectedTargetAccount = copyJobState?.target?.account; const selectedSourceAccount = copyJobState?.source?.account;
const { const {
subscriptionId: targetSubscriptionId, subscriptionId: sourceSubscriptionId,
resourceGroup: targetResourceGroup, resourceGroup: sourceResourceGroup,
accountName: targetAccountName, accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id) || {}; } = getAccountDetailsFromResourceId(selectedSourceAccount?.id) || {};
const updatedAccount = await updateIdentityFn(targetSubscriptionId, targetResourceGroup, targetAccountName); const updatedAccount = await updateIdentityFn(sourceSubscriptionId, sourceResourceGroup, sourceAccountName);
if (updatedAccount) { if (updatedAccount) {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
...prevState, ...prevState,
target: { ...prevState.target, account: updatedAccount }, source: { ...prevState.source, account: updatedAccount },
})); }));
} }
} catch (error) { } catch (error) {
@@ -46,7 +46,7 @@ const useManagedIdentity = (
setContextError(errorMessage); setContextError(errorMessage);
setLoading(false); setLoading(false);
} }
}, [copyJobState?.target?.account?.id, updateIdentityFn, setCopyJobState]); }, [copyJobState?.source?.account?.id, updateIdentityFn, setCopyJobState]);
return { loading, handleAddSystemIdentity }; return { loading, handleAddSystemIdentity };
}; };
@@ -13,7 +13,7 @@ import {
import { CopyJobContextState } from "../../../../Types/CopyJobTypes"; import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache"; import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache";
import usePermissionSections, { import usePermissionSections, {
checkTargetHasReaderRoleOnSource, checkTargetHasReadWriteRoleOnSource,
PermissionGroupConfig, PermissionGroupConfig,
SECTION_IDS, SECTION_IDS,
} from "./usePermissionsSection"; } from "./usePermissionsSection";
@@ -40,12 +40,12 @@ jest.mock("../AddManagedIdentity", () => {
return MockAddManagedIdentity; return MockAddManagedIdentity;
}); });
jest.mock("../AddReadPermissionToDefaultIdentity", () => { jest.mock("../AddReadWritePermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => { const MockAddReadWritePermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">AddReadPermissionToDefaultIdentity</div>; return <div data-testid="add-read-write-permission">AddReadWritePermissionToDefaultIdentity</div>;
}; };
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity"; MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity; return MockAddReadWritePermissionToDefaultIdentity;
}); });
jest.mock("../DefaultManagedIdentity", () => { jest.mock("../DefaultManagedIdentity", () => {
@@ -133,7 +133,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscription: undefined, subscriptionId: "",
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -152,7 +152,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscriptionId: "", subscription: undefined,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -193,7 +193,7 @@ describe("usePermissionsSection", () => {
expect(capturedResult[0].sections.map((s) => s.id)).toEqual([ expect(capturedResult[0].sections.map((s) => s.id)).toEqual([
SECTION_IDS.addManagedIdentity, SECTION_IDS.addManagedIdentity,
SECTION_IDS.defaultManagedIdentity, SECTION_IDS.defaultManagedIdentity,
SECTION_IDS.readPermissionAssigned, SECTION_IDS.readWritePermissionAssigned,
]); ]);
}); });
@@ -208,7 +208,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscription: undefined, subscriptionId: "",
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -222,7 +222,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscriptionId: "", subscription: undefined,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -284,16 +284,19 @@ describe("usePermissionsSection", () => {
describe("Section validation", () => { describe("Section validation", () => {
it("should validate addManagedIdentity section correctly", async () => { it("should validate addManagedIdentity section correctly", async () => {
const stateWithSystemAssigned = createMockState({ const stateWithSystemAssigned = createMockState({
target: { source: {
account: { account: {
id: "target-account-id", id: "source-account-id",
name: "target-account", name: "source-account",
identity: { identity: {
type: IdentityType.SystemAssigned, type: IdentityType.SystemAssigned,
principalId: "principal-123", principalId: "principal-123",
}, },
properties: { properties: {
defaultIdentity: DefaultIdentityType.FirstPartyIdentity, backupPolicy: {
type: BackupPolicyType.Periodic,
},
capabilities: [],
}, },
location: "", location: "",
type: "", type: "",
@@ -322,16 +325,20 @@ describe("usePermissionsSection", () => {
it("should validate defaultManagedIdentity section correctly", async () => { it("should validate defaultManagedIdentity section correctly", async () => {
const stateWithSystemAssignedIdentity = createMockState({ const stateWithSystemAssignedIdentity = createMockState({
target: { source: {
account: { account: {
id: "target-account-id", id: "source-account-id",
name: "target-account", name: "source-account",
identity: { identity: {
type: IdentityType.SystemAssigned, type: IdentityType.SystemAssigned,
principalId: "principal-123", principalId: "principal-123",
}, },
properties: { properties: {
defaultIdentity: DefaultIdentityType.SystemAssignedIdentity, defaultIdentity: DefaultIdentityType.SystemAssignedIdentity,
backupPolicy: {
type: BackupPolicyType.Periodic,
},
capabilities: [],
}, },
location: "", location: "",
type: "", type: "",
@@ -358,16 +365,17 @@ describe("usePermissionsSection", () => {
expect(defaultManagedIdentitySection?.completed).toBe(true); expect(defaultManagedIdentitySection?.completed).toBe(true);
}); });
it("should validate readPermissionAssigned section with reader role", async () => { it("should validate readWritePermissionAssigned section with contributor role", async () => {
const mockRoleDefinitions: RbacUtils.RoleDefinitionType[] = [ const mockRoleDefinitions: RbacUtils.RoleDefinitionType[] = [
{ {
id: "role-1", id: "role-1",
name: "Custom Role", name: "00000000-0000-0000-0000-000000000002",
permissions: [ permissions: [
{ {
dataActions: [ dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata", "Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read", "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
], ],
}, },
], ],
@@ -383,16 +391,20 @@ describe("usePermissionsSection", () => {
mockedRbacUtils.fetchRoleDefinitions.mockResolvedValue(mockRoleDefinitions); mockedRbacUtils.fetchRoleDefinitions.mockResolvedValue(mockRoleDefinitions);
const state = createMockState({ const state = createMockState({
target: { source: {
account: { account: {
id: "target-account-id", id: "source-account-id",
name: "target-account", name: "source-account",
identity: { identity: {
type: IdentityType.SystemAssigned, type: IdentityType.SystemAssigned,
principalId: "principal-123", principalId: "principal-123",
}, },
properties: { properties: {
defaultIdentity: DefaultIdentityType.SystemAssignedIdentity, defaultIdentity: DefaultIdentityType.SystemAssignedIdentity,
backupPolicy: {
type: BackupPolicyType.Periodic,
},
capabilities: [],
}, },
location: "", location: "",
type: "", type: "",
@@ -407,7 +419,9 @@ describe("usePermissionsSection", () => {
render(<TestWrapper state={state} onResult={noop} />); render(<TestWrapper state={state} onResult={noop} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.readPermissionAssigned}-completed`)).toHaveTextContent("true"); expect(screen.getByTestId(`section-${SECTION_IDS.readWritePermissionAssigned}-completed`)).toHaveTextContent(
"true",
);
}); });
expect(mockedRbacUtils.fetchRoleAssignments).toHaveBeenCalledWith( expect(mockedRbacUtils.fetchRoleAssignments).toHaveBeenCalledWith(
@@ -435,7 +449,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscription: undefined, subscriptionId: "",
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -476,7 +490,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscription: undefined, subscriptionId: "",
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -546,7 +560,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscriptionId: "", subscription: undefined,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -568,12 +582,12 @@ describe("usePermissionsSection", () => {
}); });
}); });
describe("checkTargetHasReaderRoleOnSource", () => { describe("checkTargetHasReadWriteRoleOnSource", () => {
it("should return true for built-in Reader role", () => { it("should return true for built-in Contributor role", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [ const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{ {
id: "role-1", id: "role-1",
name: "00000000-0000-0000-0000-000000000001", name: "00000000-0000-0000-0000-000000000002",
permissions: [], permissions: [],
assignableScopes: [], assignableScopes: [],
resourceGroup: "", resourceGroup: "",
@@ -583,20 +597,21 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true); expect(result).toBe(true);
}); });
it("should return true for custom role with required data actions", () => { it("should return true for custom role with read-write data actions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [ const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{ {
id: "role-1", id: "role-1",
name: "Custom Reader Role", name: "Custom Contributor Role",
permissions: [ permissions: [
{ {
dataActions: [ dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata", "Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read", "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
], ],
}, },
], ],
@@ -608,7 +623,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true); expect(result).toBe(true);
}); });
@@ -630,12 +645,12 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("should return false for empty role definitions", () => { it("should return false for empty role definitions", () => {
const result = checkTargetHasReaderRoleOnSource([]); const result = checkTargetHasReadWriteRoleOnSource([]);
expect(result).toBe(false); expect(result).toBe(false);
}); });
@@ -653,11 +668,11 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("should handle multiple roles and return true if any has sufficient permissions", () => { it("should handle multiple roles and return true if any has sufficient read-write permissions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [ const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{ {
id: "role-1", id: "role-1",
@@ -675,7 +690,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
{ {
id: "role-2", id: "role-2",
name: "00000000-0000-0000-0000-000000000001", name: "00000000-0000-0000-0000-000000000002",
permissions: [], permissions: [],
assignableScopes: [], assignableScopes: [],
resourceGroup: "", resourceGroup: "",
@@ -685,7 +700,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true); expect(result).toBe(true);
}); });
}); });
@@ -1,7 +1,7 @@
import { Keys, t } from "Localization";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { CapabilityNames } from "../../../../../../Common/Constants"; import { CapabilityNames } from "../../../../../../Common/Constants";
import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils"; import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { getAccountDetailsFromResourceId, getContainerIdentifiers, isIntraAccountCopy } from "../../../../CopyJobUtils"; import { getAccountDetailsFromResourceId, getContainerIdentifiers, isIntraAccountCopy } from "../../../../CopyJobUtils";
import { import {
BackupPolicyType, BackupPolicyType,
@@ -12,7 +12,7 @@ import {
import { CopyJobContextState } from "../../../../Types/CopyJobTypes"; import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache"; import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
import AddManagedIdentity from "../AddManagedIdentity"; import AddManagedIdentity from "../AddManagedIdentity";
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity"; import AddReadWritePermissionToDefaultIdentity from "../AddReadWritePermissionToDefaultIdentity";
import DefaultManagedIdentity from "../DefaultManagedIdentity"; import DefaultManagedIdentity from "../DefaultManagedIdentity";
import OnlineCopyEnabled from "../OnlineCopyEnabled"; import OnlineCopyEnabled from "../OnlineCopyEnabled";
import PointInTimeRestore from "../PointInTimeRestore"; import PointInTimeRestore from "../PointInTimeRestore";
@@ -36,58 +36,60 @@ export interface PermissionGroupConfig {
export const SECTION_IDS = { export const SECTION_IDS = {
addManagedIdentity: "addManagedIdentity", addManagedIdentity: "addManagedIdentity",
defaultManagedIdentity: "defaultManagedIdentity", defaultManagedIdentity: "defaultManagedIdentity",
readPermissionAssigned: "readPermissionAssigned", readWritePermissionAssigned: "readWritePermissionAssigned",
pointInTimeRestore: "pointInTimeRestore", pointInTimeRestore: "pointInTimeRestore",
onlineCopyEnabled: "onlineCopyEnabled", onlineCopyEnabled: "onlineCopyEnabled",
} as const; } as const;
const COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID = "00000000-0000-0000-0000-000000000002";
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
{ {
id: SECTION_IDS.addManagedIdentity, id: SECTION_IDS.addManagedIdentity,
title: ContainerCopyMessages.addManagedIdentity.title, title: t(Keys.containerCopy.addManagedIdentity.title),
Component: AddManagedIdentity, Component: AddManagedIdentity,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
const targetAccountIdentityType = (state?.target?.account?.identity?.type ?? "").toLowerCase(); const sourceAccountIdentityType = (state?.source?.account?.identity?.type ?? "").toLowerCase();
return ( return (
targetAccountIdentityType === IdentityType.SystemAssigned || sourceAccountIdentityType === IdentityType.SystemAssigned ||
targetAccountIdentityType === IdentityType.UserAssigned sourceAccountIdentityType === IdentityType.UserAssigned
); );
}, },
}, },
{ {
id: SECTION_IDS.defaultManagedIdentity, id: SECTION_IDS.defaultManagedIdentity,
title: ContainerCopyMessages.defaultManagedIdentity.title, title: t(Keys.containerCopy.defaultManagedIdentity.title),
Component: DefaultManagedIdentity, Component: DefaultManagedIdentity,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase(); const sourceAccountDefaultIdentity = (state?.source?.account?.properties?.defaultIdentity ?? "").toLowerCase();
return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity; return sourceAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
}, },
}, },
{ {
id: SECTION_IDS.readPermissionAssigned, id: SECTION_IDS.readWritePermissionAssigned,
title: ContainerCopyMessages.readPermissionAssigned.title, title: t(Keys.containerCopy.readWritePermissionAssigned.title),
Component: AddReadPermissionToDefaultIdentity, Component: AddReadWritePermissionToDefaultIdentity,
disabled: true, disabled: true,
validate: async (state: CopyJobContextState) => { validate: async (state: CopyJobContextState) => {
const principalId = state?.target?.account?.identity?.principalId; const principalId = state?.source?.account?.identity?.principalId;
const selectedSourceAccount = state?.source?.account; const selectedTargetAccount = state?.target?.account;
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: targetSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: targetResourceGroup,
accountName: sourceAccountName, accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
const rolesAssigned = await fetchRoleAssignments( const rolesAssigned = await fetchRoleAssignments(
sourceSubscriptionId, targetSubscriptionId,
sourceResourceGroup, targetResourceGroup,
sourceAccountName, targetAccountName,
principalId, principalId,
); );
const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []); const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []);
return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []); return checkTargetHasReadWriteRoleOnSource(roleDefinitions ?? []);
}, },
}, },
]; ];
@@ -95,7 +97,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
{ {
id: SECTION_IDS.pointInTimeRestore, id: SECTION_IDS.pointInTimeRestore,
title: ContainerCopyMessages.pointInTimeRestore.title, title: t(Keys.containerCopy.pointInTimeRestore.title),
Component: PointInTimeRestore, Component: PointInTimeRestore,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
@@ -105,7 +107,7 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
}, },
{ {
id: SECTION_IDS.onlineCopyEnabled, id: SECTION_IDS.onlineCopyEnabled,
title: ContainerCopyMessages.onlineCopyEnabled.title, title: t(Keys.containerCopy.onlineCopyEnabled.title),
Component: OnlineCopyEnabled, Component: OnlineCopyEnabled,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
@@ -119,18 +121,34 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
]; ];
/** /**
* Checks if the user has the Reader role based on role definitions. * Checks if the user has contributor-style read-write access on the source account.
*/ */
export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean { export function checkTargetHasReadWriteRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some( return roleDefinitions?.some((role) => {
(role) => if (role.name === COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID) {
role.name === "00000000-0000-0000-0000-000000000001" || return true;
role.permissions.some( }
(permission) =>
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata") && const dataActions = role.permissions?.flatMap((permission) => permission.dataActions ?? []) ?? [];
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read"),
), const hasAccountWildcard = dataActions.includes("Microsoft.DocumentDB/databaseAccounts/*");
); const hasContainerWildcard =
hasAccountWildcard || dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*");
const hasItemsWildcard =
hasContainerWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*");
const hasAccountReadMetadata =
hasAccountWildcard || dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata");
const hasItemRead =
hasItemsWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read");
const hasItemWrite =
hasItemsWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write");
return hasAccountReadMetadata && hasItemRead && hasItemWrite;
});
} }
/** /**
@@ -194,11 +212,11 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
if (crossAccountSections.length > 0) { if (crossAccountSections.length > 0) {
groups.push({ groups.push({
id: "crossAccountConfigs", id: "crossAccountConfigs",
title: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.title, title: t(Keys.containerCopy.assignPermissions.crossAccountConfiguration.title),
description: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.description( description: t(Keys.containerCopy.assignPermissions.crossAccountConfiguration.description, {
sourceAccountName, sourceAccount: sourceAccountName,
targetAccountName, destinationAccount: targetAccountName,
), }),
sections: crossAccountSections, sections: crossAccountSections,
}); });
} }
@@ -206,8 +224,10 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
if (state.migrationType === CopyJobMigrationType.Online) { if (state.migrationType === CopyJobMigrationType.Online) {
groups.push({ groups.push({
id: "onlineConfigs", id: "onlineConfigs",
title: ContainerCopyMessages.assignPermissions.onlineConfiguration.title, title: t(Keys.containerCopy.assignPermissions.onlineConfiguration.title),
description: ContainerCopyMessages.assignPermissions.onlineConfiguration.description(sourceAccountName), description: t(Keys.containerCopy.assignPermissions.onlineConfiguration.description, {
accountName: sourceAccountName,
}),
sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS], sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS],
}); });
} }
@@ -1,7 +1,7 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { Keys, t } from "Localization";
import PopoverMessage from "./PopoverContainer"; import PopoverMessage from "./PopoverContainer";
jest.mock("../../../../../Common/LoadingOverlay", () => { jest.mock("../../../../../Common/LoadingOverlay", () => {
@@ -181,7 +181,7 @@ describe("PopoverMessage Component", () => {
it("should use correct loading overlay label", () => { it("should use correct loading overlay label", () => {
render(<PopoverMessage {...defaultProps} isLoading={true} />); render(<PopoverMessage {...defaultProps} isLoading={true} />);
const loadingOverlay = screen.getByTestId("loading-overlay"); const loadingOverlay = screen.getByTestId("loading-overlay");
expect(loadingOverlay).toHaveAttribute("aria-label", ContainerCopyMessages.popoverOverlaySpinnerLabel); expect(loadingOverlay).toHaveAttribute("aria-label", t(Keys.containerCopy.popoverOverlaySpinnerLabel));
}); });
}); });
@@ -1,9 +1,9 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react"; import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import LoadingOverlay from "../../../../../Common/LoadingOverlay"; import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
interface PopoverContainerProps { interface PopoverContainerProps {
isLoading?: boolean; isLoading?: boolean;
@@ -22,7 +22,7 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
tokens={{ childrenGap: 20 }} tokens={{ childrenGap: 20 }}
style={{ maxWidth: 450 }} style={{ maxWidth: 450 }}
> >
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} /> <LoadingOverlay isLoading={isLoading} label={t(Keys.containerCopy.popoverOverlaySpinnerLabel)} />
<Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}> <Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}>
{title} {title}
</Text> </Text>
@@ -4,8 +4,8 @@ import { CopyJobMigrationType } from "Explorer/ContainerCopy/Enums/CopyJobEnums"
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes"; import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import AddCollectionPanelWrapper from "./AddCollectionPanelWrapper"; import AddCollectionPanelWrapper from "./AddCollectionPanelWrapper";
@@ -81,7 +81,7 @@ describe("AddCollectionPanelWrapper", () => {
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}, },
setCopyJobState: mockSetCopyJobState, setCopyJobState: mockSetCopyJobState,
flow: null, flow: null,
@@ -109,7 +109,9 @@ describe("AddCollectionPanelWrapper", () => {
expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument(); expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument();
expect(container.querySelector(".addCollectionPanelHeader")).toBeInTheDocument(); expect(container.querySelector(".addCollectionPanelHeader")).toBeInTheDocument();
expect(container.querySelector(".addCollectionPanelBody")).toBeInTheDocument(); expect(container.querySelector(".addCollectionPanelBody")).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.createNewContainerSubHeading)).toBeInTheDocument(); expect(
screen.getByText(t(Keys.containerCopy.selectContainers.createNewContainerSubHeadingDefault)),
).toBeInTheDocument();
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument(); expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
}); });
@@ -138,7 +140,7 @@ describe("AddCollectionPanelWrapper", () => {
it("should set header text to create container heading on mount", () => { it("should set header text to create container heading on mount", () => {
render(<AddCollectionPanelWrapper />); render(<AddCollectionPanelWrapper />);
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createContainerHeading); expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.selectContainers.createContainerHeading));
}); });
it("should reset header text to create copy job panel title on unmount", () => { it("should reset header text to create copy job panel title on unmount", () => {
@@ -146,13 +148,13 @@ describe("AddCollectionPanelWrapper", () => {
unmount(); unmount();
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createCopyJobPanelTitle); expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.createCopyJob.panelTitle));
}); });
it("should not change header text if already set correctly", () => { it("should not change header text if already set correctly", () => {
const modifiedSidePanelState = { const modifiedSidePanelState = {
...mockSidePanelState, ...mockSidePanelState,
headerText: ContainerCopyMessages.createContainerHeading, headerText: t(Keys.containerCopy.selectContainers.createContainerHeading),
}; };
mockUseSidePanel.getState = jest.fn().mockReturnValue(modifiedSidePanelState); mockUseSidePanel.getState = jest.fn().mockReturnValue(modifiedSidePanelState);
@@ -245,10 +247,10 @@ describe("AddCollectionPanelWrapper", () => {
describe("Component Lifecycle", () => { describe("Component Lifecycle", () => {
it("should properly cleanup on unmount", () => { it("should properly cleanup on unmount", () => {
const { unmount } = render(<AddCollectionPanelWrapper />); const { unmount } = render(<AddCollectionPanelWrapper />);
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createContainerHeading); expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.selectContainers.createContainerHeading));
mockSetHeaderText.mockClear(); mockSetHeaderText.mockClear();
unmount(); unmount();
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createCopyJobPanelTitle); expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.createCopyJob.panelTitle));
}); });
it("should re-render correctly when props change", () => { it("should re-render correctly when props change", () => {
@@ -1,11 +1,14 @@
import { Stack, Text } from "@fluentui/react"; import { IDropdownOption, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { readDatabasesWithARM } from "Common/dataAccess/readDatabases";
import { AccountOverride } from "Contracts/DataModels";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { produce } from "immer"; import { produce } from "immer";
import React, { useCallback, useEffect } from "react"; import { Keys, t } from "Localization";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { AddCollectionPanel } from "../../../../Panes/AddCollectionPanel/AddCollectionPanel"; import { AddCollectionPanel } from "../../../../Panes/AddCollectionPanel/AddCollectionPanel";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
type AddCollectionPanelWrapperProps = { type AddCollectionPanelWrapperProps = {
explorer?: Explorer; explorer?: Explorer;
@@ -13,18 +16,88 @@ type AddCollectionPanelWrapperProps = {
}; };
const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapperProps> = ({ explorer, goBack }) => { const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapperProps> = ({ explorer, goBack }) => {
const { setCopyJobState } = useCopyJobContext(); const { setCopyJobState, copyJobState } = useCopyJobContext();
const [destinationDatabases, setDestinationDatabases] = useState<IDropdownOption[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [permissionError, setPermissionError] = useState<string | null>(null);
const targetAccountOverride: AccountOverride | undefined = useMemo(() => {
const accountId = copyJobState?.target?.account?.id;
if (!accountId) {
return undefined;
}
const details = getAccountDetailsFromResourceId(accountId);
if (!details?.subscriptionId || !details?.resourceGroup || !details?.accountName) {
return undefined;
}
return {
subscriptionId: details.subscriptionId,
resourceGroup: details.resourceGroup,
accountName: details.accountName,
capabilities: copyJobState?.target?.account?.properties?.capabilities ?? [],
capacityMode: copyJobState?.target?.account?.properties?.capacityMode,
enableFreeTier: copyJobState?.target?.account?.properties?.enableFreeTier,
enableAnalyticalStorage: copyJobState?.target?.account?.properties?.enableAnalyticalStorage,
};
}, [copyJobState?.target?.account?.id]);
useEffect(() => { useEffect(() => {
const sidePanelStore = useSidePanel.getState(); const sidePanelStore = useSidePanel.getState();
if (sidePanelStore.headerText !== ContainerCopyMessages.createContainerHeading) { if (sidePanelStore.headerText !== t(Keys.containerCopy.selectContainers.createContainerHeading)) {
sidePanelStore.setHeaderText(ContainerCopyMessages.createContainerHeading); sidePanelStore.setHeaderText(t(Keys.containerCopy.selectContainers.createContainerHeading));
} }
return () => { return () => {
sidePanelStore.setHeaderText(ContainerCopyMessages.createCopyJobPanelTitle); sidePanelStore.setHeaderText(t(Keys.containerCopy.createCopyJob.panelTitle));
}; };
}, []); }, []);
useEffect(() => {
if (!targetAccountOverride) {
setIsLoading(false);
return undefined;
}
let cancelled = false;
const fetchDatabases = async () => {
setIsLoading(true);
setPermissionError(null);
try {
const databases = await readDatabasesWithARM({
subscriptionId: targetAccountOverride.subscriptionId,
resourceGroup: targetAccountOverride.resourceGroup,
accountName: targetAccountOverride.accountName,
apiType: "SQL",
});
if (!cancelled) {
setDestinationDatabases(databases.map((db) => ({ key: db.id, text: db.id })));
}
} catch (error) {
if (!cancelled) {
const message = error?.message || String(error);
if (message.includes("AuthorizationFailed") || message.includes("403")) {
setPermissionError(
`You do not have sufficient permissions to access the destination account "${targetAccountOverride.accountName}". ` +
"Please ensure you have at least Contributor or Owner access to create databases and containers.",
);
} else {
setPermissionError(
`Failed to load databases from the destination account "${targetAccountOverride.accountName}": ${message}`,
);
}
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
};
fetchDatabases();
return () => {
cancelled = true;
};
}, [targetAccountOverride]);
const handleAddCollectionSuccess = useCallback( const handleAddCollectionSuccess = useCallback(
(collectionData: { databaseId: string; collectionId: string }) => { (collectionData: { databaseId: string; collectionId: string }) => {
setCopyJobState( setCopyJobState(
@@ -38,13 +111,41 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
[goBack], [goBack],
); );
if (isLoading) {
return (
<Stack horizontalAlign="center" verticalAlign="center" styles={{ root: { padding: 20 } }}>
<Spinner size={SpinnerSize.large} label="Loading destination account databases..." />
</Stack>
);
}
if (permissionError) {
return (
<Stack styles={{ root: { padding: 20 } }}>
<MessageBar messageBarType={MessageBarType.error}>{permissionError}</MessageBar>
</Stack>
);
}
return ( return (
<Stack className="addCollectionPanelWrapper"> <Stack className="addCollectionPanelWrapper">
<Stack.Item className="addCollectionPanelHeader"> <Stack.Item className="addCollectionPanelHeader">
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text> <Text className="themeText">
{targetAccountOverride?.accountName
? t(Keys.containerCopy.selectContainers.createNewContainerSubHeading, {
accountName: targetAccountOverride.accountName,
})
: t(Keys.containerCopy.selectContainers.createNewContainerSubHeadingDefault)}
</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}
targetAccountOverride={targetAccountOverride}
externalDatabaseOptions={destinationDatabases}
/>
</Stack.Item> </Stack.Item>
</Stack> </Stack>
); );
@@ -3,19 +3,19 @@
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`] = ` exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`] = `
<div> <div>
<div <div
class="ms-Stack addCollectionPanelWrapper css-109" class="ms-Stack addCollectionPanelWrapper css-115"
> >
<div <div
class="ms-StackItem addCollectionPanelHeader css-110" class="ms-StackItem addCollectionPanelHeader css-116"
> >
<span <span
class="themeText css-111" class="themeText css-117"
> >
Select the properties for your container. Configure the properties for the new container.
</span> </span>
</div> </div>
<div <div
class="ms-StackItem addCollectionPanelBody css-110" class="ms-StackItem addCollectionPanelBody css-116"
> >
<div <div
data-testid="add-collection-panel" data-testid="add-collection-panel"
@@ -44,19 +44,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with both props 1`] = ` exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with both props 1`] = `
<div> <div>
<div <div
class="ms-Stack addCollectionPanelWrapper css-109" class="ms-Stack addCollectionPanelWrapper css-115"
> >
<div <div
class="ms-StackItem addCollectionPanelHeader css-110" class="ms-StackItem addCollectionPanelHeader css-116"
> >
<span <span
class="themeText css-111" class="themeText css-117"
> >
Select the properties for your container. Configure the properties for the new container.
</span> </span>
</div> </div>
<div <div
class="ms-StackItem addCollectionPanelBody css-110" class="ms-StackItem addCollectionPanelBody css-116"
> >
<div <div
data-testid="add-collection-panel" data-testid="add-collection-panel"
@@ -85,19 +85,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with explorer prop 1`] = ` exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with explorer prop 1`] = `
<div> <div>
<div <div
class="ms-Stack addCollectionPanelWrapper css-109" class="ms-Stack addCollectionPanelWrapper css-115"
> >
<div <div
class="ms-StackItem addCollectionPanelHeader css-110" class="ms-StackItem addCollectionPanelHeader css-116"
> >
<span <span
class="themeText css-111" class="themeText css-117"
> >
Select the properties for your container. Configure the properties for the new container.
</span> </span>
</div> </div>
<div <div
class="ms-StackItem addCollectionPanelBody css-110" class="ms-StackItem addCollectionPanelBody css-116"
> >
<div <div
data-testid="add-collection-panel" data-testid="add-collection-panel"
@@ -126,19 +126,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with goBack prop 1`] = ` exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with goBack prop 1`] = `
<div> <div>
<div <div
class="ms-Stack addCollectionPanelWrapper css-109" class="ms-Stack addCollectionPanelWrapper css-115"
> >
<div <div
class="ms-StackItem addCollectionPanelHeader css-110" class="ms-StackItem addCollectionPanelHeader css-116"
> >
<span <span
class="themeText css-111" class="themeText css-117"
> >
Select the properties for your container. Configure the properties for the new container.
</span> </span>
</div> </div>
<div <div
class="ms-StackItem addCollectionPanelBody css-110" class="ms-StackItem addCollectionPanelBody css-116"
> >
<div <div
data-testid="add-collection-panel" data-testid="add-collection-panel"
@@ -87,18 +87,18 @@ describe("PreviewCopyJob", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: mockSubscription, subscriptionId: "test-subscription-id",
account: mockDatabaseAccount, account: mockDatabaseAccount,
databaseId: "source-database", databaseId: "source-database",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "test-subscription-id", subscription: mockSubscription,
account: mockDatabaseAccount, account: mockDatabaseAccount,
databaseId: "target-database", databaseId: "target-database",
containerId: "target-container", containerId: "target-container",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
...overrides, ...overrides,
}; };
@@ -146,7 +146,7 @@ describe("PreviewCopyJob", () => {
it("should render with missing source subscription information", () => { it("should render with missing source subscription information", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
source: { source: {
subscription: undefined, subscriptionId: "",
account: mockDatabaseAccount, account: mockDatabaseAccount,
databaseId: "source-database", databaseId: "source-database",
containerId: "source-container", containerId: "source-container",
@@ -165,7 +165,7 @@ describe("PreviewCopyJob", () => {
it("should render with missing source account information", () => { it("should render with missing source account information", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
source: { source: {
subscription: mockSubscription, subscriptionId: "test-subscription-id",
account: null, account: null,
databaseId: "source-database", databaseId: "source-database",
containerId: "source-container", containerId: "source-container",
@@ -184,13 +184,13 @@ describe("PreviewCopyJob", () => {
it("should render with undefined database and container names", () => { it("should render with undefined database and container names", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
source: { source: {
subscription: mockSubscription, subscriptionId: "test-subscription-id",
account: mockDatabaseAccount, account: mockDatabaseAccount,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { target: {
subscriptionId: "test-subscription-id", subscription: mockSubscription,
account: mockDatabaseAccount, account: mockDatabaseAccount,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
@@ -219,7 +219,7 @@ describe("PreviewCopyJob", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
source: { source: {
subscription: longNameSubscription, subscriptionId: longNameSubscription.subscriptionId,
account: longNameAccount, account: longNameAccount,
databaseId: "long-database-name-for-testing-purposes", databaseId: "long-database-name-for-testing-purposes",
containerId: "long-container-name-for-testing-purposes", containerId: "long-container-name-for-testing-purposes",
@@ -253,13 +253,13 @@ describe("PreviewCopyJob", () => {
it("should handle special characters in database and container names", () => { it("should handle special characters in database and container names", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
source: { source: {
subscription: mockSubscription, subscriptionId: "test-subscription-id",
account: mockDatabaseAccount, account: mockDatabaseAccount,
databaseId: "test-db_with@special#chars", databaseId: "test-db_with@special#chars",
containerId: "test-container_with@special#chars", containerId: "test-container_with@special#chars",
}, },
target: { target: {
subscriptionId: "test-subscription-id", subscription: mockSubscription,
account: mockDatabaseAccount, account: mockDatabaseAccount,
databaseId: "target-db_with@special#chars", databaseId: "target-db_with@special#chars",
containerId: "target-container_with@special#chars", containerId: "target-container_with@special#chars",
@@ -285,12 +285,12 @@ describe("PreviewCopyJob", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
target: { target: {
subscriptionId: "target-subscription-id", subscription: mockSubscription,
account: targetAccount, account: targetAccount,
databaseId: "target-database", databaseId: "target-database",
containerId: "target-container", containerId: "target-container",
}, },
sourceReadAccessFromTarget: true, sourceReadWriteAccessFromTarget: true,
}); });
const { container } = render( const { container } = render(
@@ -350,7 +350,7 @@ describe("PreviewCopyJob", () => {
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
}); });
it("should display proper field labels from ContainerCopyMessages", () => { it("should display proper field labels", () => {
const mockContext = createMockContext(); const mockContext = createMockContext();
const { getByText } = render( const { getByText } = render(
@@ -360,7 +360,7 @@ describe("PreviewCopyJob", () => {
); );
expect(getByText(/Job name/i)).toBeInTheDocument(); expect(getByText(/Job name/i)).toBeInTheDocument();
expect(getByText(/Source subscription/i)).toBeInTheDocument(); expect(getByText(/Destination subscription/i)).toBeInTheDocument();
expect(getByText(/Source account/i)).toBeInTheDocument(); expect(getByText(/Destination account/i)).toBeInTheDocument();
}); });
}); });
@@ -1,6 +1,6 @@
import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from "@fluentui/react"; import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from "@fluentui/react";
import { Keys, t } from "Localization";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getDefaultJobName } from "../../../CopyJobUtils"; import { getDefaultJobName } from "../../../CopyJobUtils";
import FieldRow from "../Components/FieldRow"; import FieldRow from "../Components/FieldRow";
@@ -32,19 +32,19 @@ const PreviewCopyJob: React.FC = () => {
}; };
return ( return (
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob"> <Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
<FieldRow label={ContainerCopyMessages.jobNameLabel}> <FieldRow label={t(Keys.containerCopy.preview.jobNameLabel)}>
<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 themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text> <Text className="bold themeText">{t(Keys.containerCopy.preview.subscriptionLabel)}</Text>
<Text data-test="source-subscription-name" className="themeText"> <Text data-test="destination-subscription-name" className="themeText">
{copyJobState.source?.subscription?.displayName} {copyJobState.target?.subscription?.displayName}
</Text> </Text>
</Stack> </Stack>
<Stack> <Stack>
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text> <Text className="bold themeText">{t(Keys.containerCopy.preview.accountLabel)}</Text>
<Text data-test="source-account-name" className="themeText"> <Text data-test="destination-account-name" className="themeText">
{copyJobState.source?.account?.name} {copyJobState.target?.account?.name}
</Text> </Text>
</Stack> </Stack>
<Stack> <Stack>
@@ -1,5 +1,5 @@
import { IColumn } from "@fluentui/react"; import { IColumn } from "@fluentui/react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import { Keys, t } from "Localization";
const commonProps = { const commonProps = {
minWidth: 130, minWidth: 130,
@@ -17,25 +17,25 @@ export const getPreviewCopyJobDetailsListColumns = (): IColumn[] => {
return [ return [
{ {
key: "sourcedbname", key: "sourcedbname",
name: ContainerCopyMessages.sourceDatabaseLabel, name: t(Keys.containerCopy.preview.sourceDatabaseLabel),
fieldName: "sourceDatabaseName", fieldName: "sourceDatabaseName",
...commonProps, ...commonProps,
}, },
{ {
key: "sourcecolname", key: "sourcecolname",
name: ContainerCopyMessages.sourceContainerLabel, name: t(Keys.containerCopy.preview.sourceContainerLabel),
fieldName: "sourceContainerName", fieldName: "sourceContainerName",
...commonProps, ...commonProps,
}, },
{ {
key: "targetdbname", key: "targetdbname",
name: ContainerCopyMessages.targetDatabaseLabel, name: t(Keys.containerCopy.preview.targetDatabaseLabel),
fieldName: "targetDatabaseName", fieldName: "targetDatabaseName",
...commonProps, ...commonProps,
}, },
{ {
key: "targetcolname", key: "targetcolname",
name: ContainerCopyMessages.targetContainerLabel, name: t(Keys.containerCopy.preview.targetContainerLabel),
fieldName: "targetContainerName", fieldName: "targetContainerName",
...commonProps, ...commonProps,
}, },
@@ -49,11 +49,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-subscription-name" data-test="destination-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -64,11 +64,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-account-name" data-test="destination-account-name"
> >
test-account test-account
</span> </span>
@@ -371,11 +371,11 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-subscription-name" data-test="destination-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -386,13 +386,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-account-name" data-test="destination-account-name"
> >
test-account target-account
</span> </span>
</div> </div>
<div <div
@@ -693,11 +693,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-subscription-name" data-test="destination-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -708,11 +708,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-account-name" data-test="destination-account-name"
> >
test-account test-account
</span> </span>
@@ -1015,13 +1015,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-subscription-name" data-test="destination-subscription-name"
> >
This is a very long subscription name that might cause display issues if not handled properly Test Subscription
</span> </span>
</div> </div>
<div <div
@@ -1030,13 +1030,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-account-name" data-test="destination-account-name"
> >
this-is-a-very-long-database-account-name-that-might-cause-display-issues test-account
</span> </span>
</div> </div>
<div <div
@@ -1337,11 +1337,11 @@ exports[`PreviewCopyJob should render with missing source account information 1`
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-subscription-name" data-test="destination-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -1352,7 +1352,13 @@ exports[`PreviewCopyJob should render with missing source account information 1`
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
>
test-account
</span> </span>
</div> </div>
<div <div
@@ -1653,7 +1659,13 @@ exports[`PreviewCopyJob should render with missing source subscription informati
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
>
Test Subscription
</span> </span>
</div> </div>
<div <div
@@ -1662,11 +1674,11 @@ exports[`PreviewCopyJob should render with missing source subscription informati
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-account-name" data-test="destination-account-name"
> >
test-account test-account
</span> </span>
@@ -1969,11 +1981,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-subscription-name" data-test="destination-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -1984,11 +1996,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-account-name" data-test="destination-account-name"
> >
test-account test-account
</span> </span>
@@ -2291,11 +2303,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-subscription-name" data-test="destination-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -2306,11 +2318,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-account-name" data-test="destination-account-name"
> >
test-account test-account
</span> </span>
@@ -2613,11 +2625,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source subscription Destination subscription
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-subscription-name" data-test="destination-subscription-name"
> >
Test Subscription Test Subscription
</span> </span>
@@ -2628,11 +2640,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
<span <span
class="bold themeText css-125" class="bold themeText css-125"
> >
Source account Destination account
</span> </span>
<span <span
class="themeText css-125" class="themeText css-125"
data-test="source-account-name" data-test="destination-account-name"
> >
test-account test-account
</span> </span>
@@ -5,7 +5,7 @@ import { configContext, Platform } from "../../../../../../ConfigContext";
import { DatabaseAccount } from "../../../../../../Contracts/DataModels"; import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
import * as useDatabaseAccountsHook from "../../../../../../hooks/useDatabaseAccounts"; import * as useDatabaseAccountsHook from "../../../../../../hooks/useDatabaseAccounts";
import { apiType, userContext } from "../../../../../../UserContext"; import { apiType, userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import { Keys, t } from "Localization";
import { CopyJobContext } from "../../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes"; import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
@@ -38,6 +38,12 @@ describe("AccountDropdown", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
target: {
subscription: { subscription: {
subscriptionId: "test-subscription-id", subscriptionId: "test-subscription-id",
displayName: "Test Subscription", displayName: "Test Subscription",
@@ -46,13 +52,7 @@ describe("AccountDropdown", () => {
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { sourceReadWriteAccessFromTarget: false,
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState; } as CopyJobContextState;
const mockCopyJobContextValue = { const mockCopyJobContextValue = {
@@ -129,11 +129,11 @@ describe("AccountDropdown", () => {
renderWithContext(); renderWithContext();
expect( expect(
screen.getByText(`${ContainerCopyMessages.sourceAccountDropdownLabel}:`, { exact: true }), screen.getByText(`${t(Keys.containerCopy.selectAccount.accountDropdownLabel)}:`, { exact: true }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("combobox")).toHaveAttribute( expect(screen.getByRole("combobox")).toHaveAttribute(
"aria-label", "aria-label",
ContainerCopyMessages.sourceAccountDropdownLabel, t(Keys.containerCopy.selectAccount.accountDropdownLabel),
); );
}); });
@@ -202,7 +202,7 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0]; const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState); const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toEqual({ expect(newState.target.account).toEqual({
...mockDatabaseAccount1, ...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id), id: normalizeAccountId(mockDatabaseAccount1.id),
}); });
@@ -226,20 +226,21 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0]; const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState); const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toEqual({ expect(newState.target.account).toEqual({
...mockDatabaseAccount2, ...mockDatabaseAccount2,
id: normalizeAccountId(mockDatabaseAccount2.id), id: normalizeAccountId(mockDatabaseAccount2.id),
}); });
}); });
it("should keep current account if it exists in the filtered list", async () => { it("should keep current account if it exists in the filtered list", async () => {
const normalizedAccount1 = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
const contextWithSelectedAccount = { const contextWithSelectedAccount = {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
...mockCopyJobState, ...mockCopyJobState,
source: { target: {
...mockCopyJobState.source, ...mockCopyJobState.target,
account: mockDatabaseAccount1, account: normalizedAccount1,
}, },
}, },
}; };
@@ -256,12 +257,9 @@ describe("AccountDropdown", () => {
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState); const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
expect(newState).toEqual({ expect(newState).toEqual({
...contextWithSelectedAccount.copyJobState, ...contextWithSelectedAccount.copyJobState,
source: { target: {
...contextWithSelectedAccount.copyJobState.source, ...contextWithSelectedAccount.copyJobState.target,
account: { account: normalizedAccount1,
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
},
}, },
}); });
}); });
@@ -297,8 +295,8 @@ describe("AccountDropdown", () => {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
...mockCopyJobState, ...mockCopyJobState,
source: { target: {
...mockCopyJobState.source, ...mockCopyJobState.target,
account: portalAccount, account: portalAccount,
}, },
}, },
@@ -323,8 +321,8 @@ describe("AccountDropdown", () => {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
...mockCopyJobState, ...mockCopyJobState,
source: { target: {
...mockCopyJobState.source, ...mockCopyJobState.target,
account: hostedAccount, account: hostedAccount,
}, },
}, },
@@ -361,8 +359,8 @@ describe("AccountDropdown", () => {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
...mockCopyJobState, ...mockCopyJobState,
source: { target: {
...mockCopyJobState.source, ...mockCopyJobState.target,
subscription: null, subscription: null,
}, },
} as CopyJobContextState, } as CopyJobContextState,
@@ -376,13 +374,13 @@ describe("AccountDropdown", () => {
}); });
it("should not update state if account is already selected and the same", async () => { it("should not update state if account is already selected and the same", async () => {
const selectedAccount = mockDatabaseAccount1; const selectedAccount = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
const contextWithSelectedAccount = { const contextWithSelectedAccount = {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
...mockCopyJobState, ...mockCopyJobState,
source: { target: {
...mockCopyJobState.source, ...mockCopyJobState.target,
account: selectedAccount, account: selectedAccount,
}, },
}, },
@@ -409,7 +407,7 @@ describe("AccountDropdown", () => {
renderWithContext(); renderWithContext();
const dropdown = screen.getByRole("combobox"); const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.sourceAccountDropdownLabel); expect(dropdown).toHaveAttribute("aria-label", t(Keys.containerCopy.selectAccount.accountDropdownLabel));
}); });
it("should have required attribute", () => { it("should have required attribute", () => {
@@ -2,11 +2,11 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react"; import { Dropdown } from "@fluentui/react";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import { Keys, t } from "Localization";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { DatabaseAccount } from "../../../../../../Contracts/DataModels"; import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../../../../hooks/useDatabaseAccounts"; import { useDatabaseAccounts } from "../../../../../../hooks/useDatabaseAccounts";
import { apiType, userContext } from "../../../../../../UserContext"; import { apiType, userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
@@ -25,7 +25,7 @@ export const normalizeAccountId = (id: string = "") => {
export const AccountDropdown: React.FC<AccountDropdownProps> = () => { export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const { copyJobState, setCopyJobState } = useCopyJobContext(); const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId; const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId); const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
const sqlApiOnlyAccounts = (allAccounts || []) const sqlApiOnlyAccounts = (allAccounts || [])
.filter((account) => apiType(account) === "SQL") .filter((account) => apiType(account) === "SQL")
@@ -36,11 +36,11 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const updateCopyJobState = (newAccount: DatabaseAccount) => { const updateCopyJobState = (newAccount: DatabaseAccount) => {
setCopyJobState((prevState) => { setCopyJobState((prevState) => {
if (prevState.source?.account?.id !== newAccount.id) { if (prevState.target?.account?.id !== newAccount.id) {
return { return {
...prevState, ...prevState,
source: { target: {
...prevState.source, ...prevState.target,
account: newAccount, account: newAccount,
}, },
}; };
@@ -51,13 +51,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
useEffect(() => { useEffect(() => {
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) { if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
const currentAccountId = copyJobState?.source?.account?.id; const currentAccountId = copyJobState?.target?.account?.id;
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id); const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
const selectedAccountId = currentAccountId || predefinedAccountId; const selectedAccountId = currentAccountId || predefinedAccountId;
const targetAccount: DatabaseAccount | null = const matchedAccount: DatabaseAccount | null =
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null; sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]); updateCopyJobState(matchedAccount || sqlApiOnlyAccounts[0]);
} }
}, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]); }, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]);
@@ -77,13 +77,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
}; };
const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0; const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0;
const selectedAccountId = normalizeAccountId(copyJobState?.source?.account?.id ?? ""); const selectedAccountId = normalizeAccountId(copyJobState?.target?.account?.id ?? "");
return ( return (
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}> <FieldRow label={t(Keys.containerCopy.selectAccount.accountDropdownLabel)}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder} placeholder={t(Keys.containerCopy.selectAccount.accountDropdownPlaceholder)}
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel} ariaLabel={t(Keys.containerCopy.selectAccount.accountDropdownLabel)}
options={accountOptions} options={accountOptions}
disabled={isAccountDropdownDisabled} disabled={isAccountDropdownDisabled}
required required
@@ -1,7 +1,7 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import { Keys, t } from "Localization";
import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { MigrationType } from "./MigrationType"; import { MigrationType } from "./MigrationType";
@@ -29,7 +29,7 @@ describe("MigrationType", () => {
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}, },
setCopyJobState: mockSetCopyJobState, setCopyJobState: mockSetCopyJobState,
flow: { currentScreen: "selectAccount" }, flow: { currentScreen: "selectAccount" },
@@ -53,9 +53,9 @@ describe("MigrationType", () => {
expect(screen.getByRole("radiogroup")).toBeInTheDocument(); expect(screen.getByRole("radiogroup")).toBeInTheDocument();
const offlineRadio = screen.getByRole("radio", { const offlineRadio = screen.getByRole("radio", {
name: ContainerCopyMessages.migrationTypeOptions.offline.title, name: t(Keys.containerCopy.migrationType.offline.title),
}); });
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }); const onlineRadio = screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) });
expect(offlineRadio).toBeInTheDocument(); expect(offlineRadio).toBeInTheDocument();
expect(onlineRadio).toBeInTheDocument(); expect(onlineRadio).toBeInTheDocument();
@@ -65,9 +65,9 @@ describe("MigrationType", () => {
it("should render with online mode selected by default", () => { it("should render with online mode selected by default", () => {
render(<MigrationType />); render(<MigrationType />);
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }); const onlineRadio = screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) });
const offlineRadio = screen.getByRole("radio", { const offlineRadio = screen.getByRole("radio", {
name: ContainerCopyMessages.migrationTypeOptions.offline.title, name: t(Keys.containerCopy.migrationType.offline.title),
}); });
expect(onlineRadio).toBeChecked(); expect(onlineRadio).toBeChecked();
@@ -86,9 +86,9 @@ describe("MigrationType", () => {
render(<MigrationType />); render(<MigrationType />);
const offlineRadio = screen.getByRole("radio", { const offlineRadio = screen.getByRole("radio", {
name: ContainerCopyMessages.migrationTypeOptions.offline.title, name: t(Keys.containerCopy.migrationType.offline.title),
}); });
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }); const onlineRadio = screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) });
expect(offlineRadio).toBeChecked(); expect(offlineRadio).toBeChecked();
expect(onlineRadio).not.toBeChecked(); expect(onlineRadio).not.toBeChecked();
@@ -141,7 +141,7 @@ describe("MigrationType", () => {
render(<MigrationType />); render(<MigrationType />);
const offlineRadio = screen.getByRole("radio", { const offlineRadio = screen.getByRole("radio", {
name: ContainerCopyMessages.migrationTypeOptions.offline.title, name: t(Keys.containerCopy.migrationType.offline.title),
}); });
fireEvent.click(offlineRadio); fireEvent.click(offlineRadio);
@@ -167,7 +167,7 @@ describe("MigrationType", () => {
render(<MigrationType />); render(<MigrationType />);
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }); const onlineRadio = screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) });
fireEvent.click(onlineRadio); fireEvent.click(onlineRadio);
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
@@ -198,11 +198,9 @@ describe("MigrationType", () => {
render(<MigrationType />); render(<MigrationType />);
expect( expect(
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }), screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.offline.title) }),
).toBeInTheDocument();
expect(
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) })).toBeInTheDocument();
}); });
}); });
@@ -220,11 +218,9 @@ describe("MigrationType", () => {
expect(container.querySelector("[data-test='migration-type']")).toBeInTheDocument(); expect(container.querySelector("[data-test='migration-type']")).toBeInTheDocument();
expect( expect(
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }), screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.offline.title) }),
).toBeInTheDocument();
expect(
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) })).toBeInTheDocument();
}); });
it("should handle null copyJobState gracefully", () => { it("should handle null copyJobState gracefully", () => {
@@ -3,20 +3,20 @@
import { ChoiceGroup, IChoiceGroupOption, Stack, Text } from "@fluentui/react"; import { ChoiceGroup, IChoiceGroupOption, Stack, Text } from "@fluentui/react";
import MarkdownRender from "@nteract/markdown"; import MarkdownRender from "@nteract/markdown";
import { useCopyJobContext } from "Explorer/ContainerCopy/Context/CopyJobContext"; import { useCopyJobContext } from "Explorer/ContainerCopy/Context/CopyJobContext";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
interface MigrationTypeProps {} interface MigrationTypeProps {}
const options: IChoiceGroupOption[] = [ const options: IChoiceGroupOption[] = [
{ {
key: CopyJobMigrationType.Offline, key: CopyJobMigrationType.Offline,
text: ContainerCopyMessages.migrationTypeOptions.offline.title, text: t(Keys.containerCopy.migrationType.offline.title),
styles: { root: { width: "33%" } }, styles: { root: { width: "33%" } },
}, },
{ {
key: CopyJobMigrationType.Online, key: CopyJobMigrationType.Online,
text: ContainerCopyMessages.migrationTypeOptions.online.title, text: t(Keys.containerCopy.migrationType.online.title),
styles: { root: { width: "33%" } }, styles: { root: { width: "33%" } },
}, },
]; ];
@@ -47,8 +47,13 @@ export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
}; };
const selectedKey = copyJobState?.migrationType ?? ""; const selectedKey = copyJobState?.migrationType ?? "";
const selectedKeyLowercase = selectedKey.toLowerCase() as keyof typeof ContainerCopyMessages.migrationTypeOptions; const selectedKeyLowercase = selectedKey.toLowerCase() as "offline" | "online";
const selectedKeyContent = ContainerCopyMessages.migrationTypeOptions[selectedKeyLowercase]; const migrationTypeDescriptionKey =
selectedKeyLowercase === "offline"
? Keys.containerCopy.migrationType.offline.description
: selectedKeyLowercase === "online"
? Keys.containerCopy.migrationType.online.description
: null;
return ( return (
<Stack data-test="migration-type" className="migrationTypeContainer"> <Stack data-test="migration-type" className="migrationTypeContainer">
@@ -61,14 +66,14 @@ export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
styles={choiceGroupStyles} styles={choiceGroupStyles}
/> />
</Stack.Item> </Stack.Item>
{selectedKeyContent && ( {migrationTypeDescriptionKey && (
<Stack.Item styles={{ root: { marginTop: 10 } }}> <Stack.Item styles={{ root: { marginTop: 10 } }}>
<Text <Text
variant="small" variant="small"
className="migrationTypeDescription" className="migrationTypeDescription"
data-test={`migration-type-description-${selectedKeyLowercase}`} data-test={`migration-type-description-${selectedKeyLowercase}`}
> >
<MarkdownRender source={selectedKeyContent.description} linkTarget="_blank" /> <MarkdownRender source={t(migrationTypeDescriptionKey)} linkTarget="_blank" />
</Text> </Text>
</Stack.Item> </Stack.Item>
)} )}
@@ -8,14 +8,9 @@ import { SubscriptionDropdown } from "./SubscriptionDropdown";
jest.mock("../../../../../../hooks/useSubscriptions"); jest.mock("../../../../../../hooks/useSubscriptions");
jest.mock("../../../../../../UserContext"); jest.mock("../../../../../../UserContext");
jest.mock("../../../../ContainerCopyMessages");
const mockUseSubscriptions = jest.requireMock("../../../../../../hooks/useSubscriptions").useSubscriptions; const mockUseSubscriptions = jest.requireMock("../../../../../../hooks/useSubscriptions").useSubscriptions;
const mockUserContext = jest.requireMock("../../../../../../UserContext").userContext; const mockUserContext = jest.requireMock("../../../../../../UserContext").userContext;
const mockContainerCopyMessages = jest.requireMock("../../../../ContainerCopyMessages").default;
mockContainerCopyMessages.subscriptionDropdownLabel = "Subscription";
mockContainerCopyMessages.subscriptionDropdownPlaceholder = "Select a subscription";
describe("SubscriptionDropdown", () => { describe("SubscriptionDropdown", () => {
let mockExplorer: Explorer; let mockExplorer: Explorer;
@@ -1,11 +1,11 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react"; import { Dropdown } from "@fluentui/react";
import { Keys, t } from "Localization";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Subscription } from "../../../../../../Contracts/DataModels"; import { Subscription } from "../../../../../../Contracts/DataModels";
import { useSubscriptions } from "../../../../../../hooks/useSubscriptions"; import { useSubscriptions } from "../../../../../../hooks/useSubscriptions";
import { userContext } from "../../../../../../UserContext"; import { userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
@@ -17,11 +17,11 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
const updateCopyJobState = (newSubscription: Subscription) => { const updateCopyJobState = (newSubscription: Subscription) => {
setCopyJobState((prevState) => { setCopyJobState((prevState) => {
if (prevState.source?.subscription?.subscriptionId !== newSubscription.subscriptionId) { if (prevState.target?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
return { return {
...prevState, ...prevState,
source: { target: {
...prevState.source, ...prevState.target,
subscription: newSubscription, subscription: newSubscription,
account: null, account: null,
}, },
@@ -33,7 +33,7 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
useEffect(() => { useEffect(() => {
if (subscriptions && subscriptions.length > 0) { if (subscriptions && subscriptions.length > 0) {
const currentSubscriptionId = copyJobState?.source?.subscription?.subscriptionId; const currentSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
const predefinedSubscriptionId = userContext.subscriptionId; const predefinedSubscriptionId = userContext.subscriptionId;
const selectedSubscriptionId = currentSubscriptionId || predefinedSubscriptionId; const selectedSubscriptionId = currentSubscriptionId || predefinedSubscriptionId;
@@ -61,13 +61,13 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
} }
}; };
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId; const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
return ( return (
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}> <FieldRow label={t(Keys.containerCopy.selectAccount.subscriptionDropdownLabel)}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.subscriptionDropdownPlaceholder} placeholder={t(Keys.containerCopy.selectAccount.subscriptionDropdownPlaceholder)}
ariaLabel={ContainerCopyMessages.subscriptionDropdownLabel} ariaLabel={t(Keys.containerCopy.selectAccount.subscriptionDropdownLabel)}
data-test="subscription-dropdown" data-test="subscription-dropdown"
options={subscriptionOptions} options={subscriptionOptions}
required required
@@ -30,18 +30,18 @@ describe("SelectAccount", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Online, migrationType: CopyJobMigrationType.Online,
source: { source: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "", subscriptionId: "",
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, target: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
}, },
setCopyJobState: mockSetCopyJobState, setCopyJobState: mockSetCopyJobState,
flow: { currentScreen: "selectAccount" }, flow: { currentScreen: "selectAccount" },
@@ -68,7 +68,7 @@ describe("SelectAccount", () => {
expect(container.firstChild).toHaveAttribute("data-test", "Panel:SelectAccountContainer"); expect(container.firstChild).toHaveAttribute("data-test", "Panel:SelectAccountContainer");
expect(container.firstChild).toHaveClass("selectAccountContainer"); expect(container.firstChild).toHaveClass("selectAccountContainer");
expect(screen.getByText(/Please select a source account from which to copy/i)).toBeInTheDocument(); expect(screen.getByText(/Please select a destination account to copy to/i)).toBeInTheDocument();
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument(); expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument(); expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();
@@ -1,6 +1,6 @@
import { Stack, Text } from "@fluentui/react"; import { Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { AccountDropdown } from "./Components/AccountDropdown"; import { AccountDropdown } from "./Components/AccountDropdown";
import { MigrationType } from "./Components/MigrationType"; import { MigrationType } from "./Components/MigrationType";
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown"; import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
@@ -8,7 +8,7 @@ import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
const SelectAccount = React.memo(() => { const SelectAccount = React.memo(() => {
return ( return (
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}> <Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
<Text className="themeText">{ContainerCopyMessages.selectAccountDescription}</Text> <Text className="themeText">{t(Keys.containerCopy.selectAccount.description)}</Text>
<SubscriptionDropdown /> <SubscriptionDropdown />
@@ -8,7 +8,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
<span <span
class="themeText css-110" class="themeText css-110"
> >
Please select a source account from which to copy. Please select a destination account to copy to.
</span> </span>
<div <div
data-testid="subscription-dropdown" data-testid="subscription-dropdown"
@@ -7,19 +7,9 @@ import { dropDownChangeHandler } from "./DropDownChangeHandler";
const createMockInitialState = (): CopyJobContextState => ({ const createMockInitialState = (): CopyJobContextState => ({
jobName: "test-job", jobName: "test-job",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
source: { source: {
subscription: { subscriptionId: "source-sub-id",
subscriptionId: "source-sub-id",
displayName: "Source Subscription",
state: "Enabled",
subscriptionPolicies: {
locationPlacementId: "test",
quotaId: "test",
spendingLimit: "Off",
},
authorizationSource: "test",
},
account: { account: {
id: "source-account-id", id: "source-account-id",
name: "source-account", name: "source-account",
@@ -50,7 +40,17 @@ const createMockInitialState = (): CopyJobContextState => ({
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "target-sub-id", subscription: {
subscriptionId: "target-sub-id",
displayName: "Target Subscription",
state: "Enabled",
subscriptionPolicies: {
locationPlacementId: "test",
quotaId: "test",
spendingLimit: "Off",
},
authorizationSource: "test",
},
account: { account: {
id: "target-account-id", id: "target-account-id",
name: "target-account", name: "target-account",
@@ -169,7 +169,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.source.databaseId).toBe("new-source-db"); expect(capturedState.source.databaseId).toBe("new-source-db");
expect(capturedState.source.containerId).toBeUndefined(); expect(capturedState.source.containerId).toBeUndefined();
expect(capturedState.source.subscription).toEqual(initialState.source.subscription); expect(capturedState.source.subscriptionId).toEqual(initialState.source.subscriptionId);
expect(capturedState.source.account).toEqual(initialState.source.account); expect(capturedState.source.account).toEqual(initialState.source.account);
expect(capturedState.target).toEqual(initialState.target); expect(capturedState.target).toEqual(initialState.target);
}); });
@@ -181,7 +181,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.jobName).toBe(initialState.jobName); expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType); expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget); expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
}); });
}); });
@@ -193,7 +193,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.source.containerId).toBe("new-source-container"); expect(capturedState.source.containerId).toBe("new-source-container");
expect(capturedState.source.databaseId).toBe(initialState.source.databaseId); expect(capturedState.source.databaseId).toBe(initialState.source.databaseId);
expect(capturedState.source.subscription).toEqual(initialState.source.subscription); expect(capturedState.source.subscriptionId).toEqual(initialState.source.subscriptionId);
expect(capturedState.source.account).toEqual(initialState.source.account); expect(capturedState.source.account).toEqual(initialState.source.account);
expect(capturedState.target).toEqual(initialState.target); expect(capturedState.target).toEqual(initialState.target);
}); });
@@ -215,7 +215,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.target.databaseId).toBe("new-target-db"); expect(capturedState.target.databaseId).toBe("new-target-db");
expect(capturedState.target.containerId).toBeUndefined(); expect(capturedState.target.containerId).toBeUndefined();
expect(capturedState.target.subscriptionId).toBe(initialState.target.subscriptionId); expect(capturedState.target.subscription).toEqual(initialState.target.subscription);
expect(capturedState.target.account).toEqual(initialState.target.account); expect(capturedState.target.account).toEqual(initialState.target.account);
expect(capturedState.source).toEqual(initialState.source); expect(capturedState.source).toEqual(initialState.source);
}); });
@@ -227,7 +227,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.jobName).toBe(initialState.jobName); expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType); expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget); expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
}); });
}); });
@@ -239,7 +239,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.target.containerId).toBe("new-target-container"); expect(capturedState.target.containerId).toBe("new-target-container");
expect(capturedState.target.databaseId).toBe(initialState.target.databaseId); expect(capturedState.target.databaseId).toBe(initialState.target.databaseId);
expect(capturedState.target.subscriptionId).toBe(initialState.target.subscriptionId); expect(capturedState.target.subscription).toEqual(initialState.target.subscription);
expect(capturedState.target.account).toEqual(initialState.target.account); expect(capturedState.target.account).toEqual(initialState.target.account);
expect(capturedState.source).toEqual(initialState.source); expect(capturedState.source).toEqual(initialState.source);
}); });
@@ -15,15 +15,6 @@ jest.mock("../../../../../hooks/useDataContainers", () => ({
useDataContainers: jest.fn(), useDataContainers: jest.fn(),
})); }));
jest.mock("../../../ContainerCopyMessages", () => ({
__esModule: true,
default: {
selectSourceAndTargetContainersDescription: "Select source and target containers for migration",
sourceContainerSubHeading: "Source Container",
targetContainerSubHeading: "Target Container",
},
}));
jest.mock("./Events/DropDownChangeHandler", () => ({ jest.mock("./Events/DropDownChangeHandler", () => ({
dropDownChangeHandler: jest.fn(() => () => jest.fn()), dropDownChangeHandler: jest.fn(() => () => jest.fn()),
})); }));
@@ -73,7 +64,7 @@ describe("SelectSourceAndTargetContainers", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: { subscriptionId: "test-subscription-id" }, subscriptionId: "test-subscription-id",
account: { account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account", id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account", name: "test-account",
@@ -82,7 +73,7 @@ describe("SelectSourceAndTargetContainers", () => {
containerId: "container1", containerId: "container1",
}, },
target: { target: {
subscriptionId: "test-subscription-id", subscription: { subscriptionId: "test-subscription-id" },
account: { account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account", id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account", name: "test-account",
@@ -90,7 +81,7 @@ describe("SelectSourceAndTargetContainers", () => {
databaseId: "db2", databaseId: "db2",
containerId: "container2", containerId: "container2",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}; };
const mockMemoizedData = { const mockMemoizedData = {
@@ -124,22 +115,26 @@ describe("SelectSourceAndTargetContainers", () => {
describe("Component Rendering", () => { describe("Component Rendering", () => {
it("should render without crashing", () => { it("should render without crashing", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument(); expect(
screen.getByText("Please select a source container and a destination container to copy to."),
).toBeInTheDocument();
}); });
it("should render description text", () => { it("should render description text", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument(); expect(
screen.getByText("Please select a source container and a destination container to copy to."),
).toBeInTheDocument();
}); });
it("should render source container section", () => { it("should render source container section", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument(); expect(screen.getByText("Source container")).toBeInTheDocument();
}); });
it("should render target container section", () => { it("should render target container section", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
it("should return null when source is not available", () => { it("should return null when source is not available", () => {
@@ -238,14 +233,14 @@ describe("SelectSourceAndTargetContainers", () => {
describe("Component Props", () => { describe("Component Props", () => {
it("should pass showAddCollectionPanel to DatabaseContainerSection", () => { it("should pass showAddCollectionPanel to DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />); renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />);
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
it("should render without showAddCollectionPanel prop", () => { it("should render without showAddCollectionPanel prop", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument(); expect(screen.getByText("Source container")).toBeInTheDocument();
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
}); });
@@ -310,13 +305,13 @@ describe("SelectSourceAndTargetContainers", () => {
it("should pass correct props to source DatabaseContainerSection", () => { it("should pass correct props to source DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument(); expect(screen.getByText("Source container")).toBeInTheDocument();
}); });
it("should pass correct props to target DatabaseContainerSection", () => { it("should pass correct props to target DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />); renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />);
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
it("should disable source container dropdown when no database is selected", () => { it("should disable source container dropdown when no database is selected", () => {
@@ -329,7 +324,7 @@ describe("SelectSourceAndTargetContainers", () => {
} as ReturnType<typeof useSourceAndTargetData>); } as ReturnType<typeof useSourceAndTargetData>);
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument(); expect(screen.getByText("Source container")).toBeInTheDocument();
}); });
it("should disable target container dropdown when no database is selected", () => { it("should disable target container dropdown when no database is selected", () => {
@@ -342,7 +337,7 @@ describe("SelectSourceAndTargetContainers", () => {
} as ReturnType<typeof useSourceAndTargetData>); } as ReturnType<typeof useSourceAndTargetData>);
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
}); });
@@ -353,7 +348,9 @@ describe("SelectSourceAndTargetContainers", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument(); expect(
screen.getByText("Please select a source container and a destination container to copy to."),
).toBeInTheDocument();
}); });
it("should handle hooks throwing errors gracefully", () => { it("should handle hooks throwing errors gracefully", () => {
@@ -421,7 +418,9 @@ describe("SelectSourceAndTargetContainers", () => {
it("should apply correct spacing tokens", () => { it("should apply correct spacing tokens", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument(); expect(
screen.getByText("Please select a source container and a destination container to copy to."),
).toBeInTheDocument();
}); });
}); });
@@ -429,9 +428,9 @@ describe("SelectSourceAndTargetContainers", () => {
it("should render description, source section, and target section in correct order", () => { it("should render description, source section, and target section in correct order", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
const description = screen.getByText("Select source and target containers for migration"); const description = screen.getByText("Please select a source container and a destination container to copy to.");
const sourceSection = screen.getByText("Source Container"); const sourceSection = screen.getByText("Source container");
const targetSection = screen.getByText("Target Container"); const targetSection = screen.getByText("Destination container");
expect(description).toBeInTheDocument(); expect(description).toBeInTheDocument();
expect(sourceSection).toBeInTheDocument(); expect(sourceSection).toBeInTheDocument();
@@ -1,9 +1,9 @@
import { Stack } from "@fluentui/react"; import { Stack } from "@fluentui/react";
import { DatabaseModel } from "Contracts/DataModels"; import { DatabaseModel } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { useDatabases } from "../../../../../hooks/useDatabases"; import { useDatabases } from "../../../../../hooks/useDatabases";
import { useDataContainers } from "../../../../../hooks/useDataContainers"; import { useDataContainers } from "../../../../../hooks/useDataContainers";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { DatabaseContainerSection } from "./components/DatabaseContainerSection"; import { DatabaseContainerSection } from "./components/DatabaseContainerSection";
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler"; import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
@@ -52,9 +52,9 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
className="selectSourceAndTargetContainers" className="selectSourceAndTargetContainers"
tokens={{ childrenGap: 25 }} tokens={{ childrenGap: 25 }}
> >
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span> <span className="themeText">{t(Keys.containerCopy.selectContainers.description)}</span>
<DatabaseContainerSection <DatabaseContainerSection
heading={ContainerCopyMessages.sourceContainerSubHeading} heading={t(Keys.containerCopy.selectContainers.sourceContainerSubHeading)}
databaseOptions={sourceDatabaseOptions} databaseOptions={sourceDatabaseOptions}
selectedDatabase={source?.databaseId} selectedDatabase={source?.databaseId}
databaseDisabled={false} databaseDisabled={false}
@@ -66,7 +66,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
sectionType="source" sectionType="source"
/> />
<DatabaseContainerSection <DatabaseContainerSection
heading={ContainerCopyMessages.targetContainerSubHeading} heading={t(Keys.containerCopy.selectContainers.targetContainerSubHeading)}
databaseOptions={targetDatabaseOptions} databaseOptions={targetDatabaseOptions}
selectedDatabase={target?.databaseId} selectedDatabase={target?.databaseId}
databaseDisabled={false} databaseDisabled={false}
@@ -1,7 +1,7 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import { Keys, t } from "Localization";
import { DatabaseContainerSectionProps, DropdownOptionType } from "../../../../Types/CopyJobTypes"; import { DatabaseContainerSectionProps, DropdownOptionType } from "../../../../Types/CopyJobTypes";
import { DatabaseContainerSection } from "./DatabaseContainerSection"; import { DatabaseContainerSection } from "./DatabaseContainerSection";
@@ -60,11 +60,14 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
expect(databaseDropdown).toBeInTheDocument(); expect(databaseDropdown).toBeInTheDocument();
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel); expect(databaseDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
);
expect(databaseDropdown).not.toBeDisabled(); expect(databaseDropdown).not.toBeDisabled();
}); });
@@ -72,30 +75,35 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(containerDropdown).toBeInTheDocument(); expect(containerDropdown).toBeInTheDocument();
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel); expect(containerDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.containerDropdownLabel),
);
expect(containerDropdown).not.toBeDisabled(); expect(containerDropdown).not.toBeDisabled();
}); });
it("renders database label correctly", () => { it("renders database label correctly", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.databaseDropdownLabel}:`)).toBeInTheDocument(); expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}:`)).toBeInTheDocument();
}); });
it("renders container label correctly", () => { it("renders container label correctly", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.containerDropdownLabel}:`)).toBeInTheDocument(); expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.containerDropdownLabel)}:`)).toBeInTheDocument();
}); });
it("does not render create container button when handleOnDemandCreateContainer is not provided", () => { it("does not render create container button when handleOnDemandCreateContainer is not provided", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.queryByText(ContainerCopyMessages.createContainerButtonLabel)).not.toBeInTheDocument(); expect(
screen.queryByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel)),
).not.toBeInTheDocument();
}); });
it("renders create container button when handleOnDemandCreateContainer is provided", () => { it("renders create container button when handleOnDemandCreateContainer is provided", () => {
@@ -107,7 +115,7 @@ describe("DatabaseContainerSection", () => {
const createButton = container.querySelector(".create-container-link-btn"); const createButton = container.querySelector(".create-container-link-btn");
expect(createButton).toBeInTheDocument(); expect(createButton).toBeInTheDocument();
expect(createButton).toHaveTextContent(ContainerCopyMessages.createContainerButtonLabel); expect(createButton).toHaveTextContent(t(Keys.containerCopy.selectContainers.createContainerButtonLabel));
}); });
}); });
@@ -121,7 +129,7 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithDisabledDatabase} />); render(<DatabaseContainerSection {...propsWithDisabledDatabase} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
expect(databaseDropdown).toHaveAttribute("aria-disabled", "true"); expect(databaseDropdown).toHaveAttribute("aria-disabled", "true");
@@ -136,7 +144,7 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithDisabledContainer} />); render(<DatabaseContainerSection {...propsWithDisabledContainer} />);
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(containerDropdown).toHaveAttribute("aria-disabled", "true"); expect(containerDropdown).toHaveAttribute("aria-disabled", "true");
@@ -152,10 +160,10 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithFalsyDisabled} />); render(<DatabaseContainerSection {...propsWithFalsyDisabled} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(databaseDropdown).not.toHaveAttribute("aria-disabled", "true"); expect(databaseDropdown).not.toHaveAttribute("aria-disabled", "true");
@@ -167,21 +175,27 @@ describe("DatabaseContainerSection", () => {
it("calls databaseOnChange when database dropdown selection changes", () => { it("calls databaseOnChange when database dropdown selection changes", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
fireEvent.click(databaseDropdown); fireEvent.click(databaseDropdown);
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel); expect(databaseDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
);
}); });
it("calls containerOnChange when container dropdown selection changes", () => { it("calls containerOnChange when container dropdown selection changes", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
fireEvent.click(containerDropdown); fireEvent.click(containerDropdown);
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel); expect(containerDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.containerDropdownLabel),
);
}); });
it("calls handleOnDemandCreateContainer when create container button is clicked", () => { it("calls handleOnDemandCreateContainer when create container button is clicked", () => {
@@ -192,7 +206,7 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithCreateHandler} />); render(<DatabaseContainerSection {...propsWithCreateHandler} />);
const createButton = screen.getByText(ContainerCopyMessages.createContainerButtonLabel); const createButton = screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel));
fireEvent.click(createButton); fireEvent.click(createButton);
expect(mockHandleOnDemandCreateContainer).toHaveBeenCalledTimes(1); expect(mockHandleOnDemandCreateContainer).toHaveBeenCalledTimes(1);
@@ -235,10 +249,10 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithEmptyOptions} />); render(<DatabaseContainerSection {...propsWithEmptyOptions} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(databaseDropdown).toBeInTheDocument(); expect(databaseDropdown).toBeInTheDocument();
@@ -251,24 +265,30 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel); expect(databaseDropdown).toHaveAttribute(
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel); "aria-label",
t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
);
expect(containerDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.containerDropdownLabel),
);
}); });
it("has proper required attributes for dropdowns", () => { it("has proper required attributes for dropdowns", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(databaseDropdown).toHaveAttribute("aria-required", "true"); expect(databaseDropdown).toHaveAttribute("aria-required", "true");
@@ -278,8 +298,8 @@ describe("DatabaseContainerSection", () => {
it("maintains proper label associations", () => { it("maintains proper label associations", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.databaseDropdownLabel}:`)).toBeInTheDocument(); expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}:`)).toBeInTheDocument();
expect(screen.getByText(`${ContainerCopyMessages.containerDropdownLabel}:`)).toBeInTheDocument(); expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.containerDropdownLabel)}:`)).toBeInTheDocument();
}); });
}); });
@@ -299,7 +319,9 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...minimalProps} />); render(<DatabaseContainerSection {...minimalProps} />);
expect(screen.getByText("Test Heading")).toBeInTheDocument(); expect(screen.getByText("Test Heading")).toBeInTheDocument();
expect(screen.queryByText(ContainerCopyMessages.createContainerButtonLabel)).not.toBeInTheDocument(); expect(
screen.queryByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel)),
).not.toBeInTheDocument();
}); });
it("handles empty string selections", () => { it("handles empty string selections", () => {
@@ -366,7 +388,7 @@ describe("DatabaseContainerSection", () => {
const { container } = render(<DatabaseContainerSection {...propsWithCreateHandler} />); const { container } = render(<DatabaseContainerSection {...propsWithCreateHandler} />);
const createButton = screen.getByText(ContainerCopyMessages.createContainerButtonLabel); const createButton = screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel));
expect(createButton).toBeInTheDocument(); expect(createButton).toBeInTheDocument();
const containerSection = container.querySelector(".databaseContainerSection"); const containerSection = container.querySelector(".databaseContainerSection");
@@ -381,7 +403,7 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithCreateHandler} />); render(<DatabaseContainerSection {...propsWithCreateHandler} />);
expect(screen.getByText(ContainerCopyMessages.createContainerButtonLabel)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel))).toBeInTheDocument();
}); });
}); });
@@ -1,6 +1,6 @@
import { ActionButton, Dropdown, Stack } from "@fluentui/react"; import { ActionButton, Dropdown, Stack } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DatabaseContainerSectionProps } from "../../../../Types/CopyJobTypes"; import { DatabaseContainerSectionProps } from "../../../../Types/CopyJobTypes";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
@@ -19,10 +19,10 @@ export const DatabaseContainerSection = ({
}: DatabaseContainerSectionProps) => ( }: DatabaseContainerSectionProps) => (
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection"> <Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
<label className="subHeading">{heading}</label> <label className="subHeading">{heading}</label>
<FieldRow label={ContainerCopyMessages.databaseDropdownLabel}> <FieldRow label={t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.databaseDropdownPlaceholder} placeholder={t(Keys.containerCopy.selectContainers.databaseDropdownPlaceholder)}
ariaLabel={ContainerCopyMessages.databaseDropdownLabel} ariaLabel={t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}
options={databaseOptions} options={databaseOptions}
required required
disabled={!!databaseDisabled} disabled={!!databaseDisabled}
@@ -31,11 +31,11 @@ export const DatabaseContainerSection = ({
data-test={`${sectionType}-databaseDropdown`} data-test={`${sectionType}-databaseDropdown`}
/> />
</FieldRow> </FieldRow>
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}> <FieldRow label={t(Keys.containerCopy.selectContainers.containerDropdownLabel)}>
<Stack> <Stack>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.containerDropdownPlaceholder} placeholder={t(Keys.containerCopy.selectContainers.containerDropdownPlaceholder)}
ariaLabel={ContainerCopyMessages.containerDropdownLabel} ariaLabel={t(Keys.containerCopy.selectContainers.containerDropdownLabel)}
options={containerOptions} options={containerOptions}
required required
disabled={!!containerDisabled} disabled={!!containerDisabled}
@@ -49,7 +49,7 @@ export const DatabaseContainerSection = ({
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
onClick={() => handleOnDemandCreateContainer()} onClick={() => handleOnDemandCreateContainer()}
> >
{ContainerCopyMessages.createContainerButtonLabel} {t(Keys.containerCopy.selectContainers.createContainerButtonLabel)}
</ActionButton> </ActionButton>
)} )}
</Stack> </Stack>
@@ -69,15 +69,15 @@ describe("useSourceAndTargetData", () => {
const mockCopyJobState: CopyJobContextState = { const mockCopyJobState: CopyJobContextState = {
jobName: "test-job", jobName: "test-job",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
source: { source: {
subscription: mockSubscription, subscriptionId: "source-subscription-id",
account: mockSourceAccount, account: mockSourceAccount,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "target-subscription-id", subscription: mockSubscription,
account: mockTargetAccount, account: mockTargetAccount,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -86,13 +86,13 @@ describe("useCopyJobNavigation", () => {
jobName: "test-job", jobName: "test-job",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: { subscriptionId: "source-sub-id" } as any, subscriptionId: "source-sub-id",
account: { id: "source-account-id", name: "Account-1" } as any, account: { id: "source-account-id", name: "Account-1" } as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "target-sub-id", subscription: { subscriptionId: "target-sub-id" } as any,
account: { id: "target-account-id", name: "Account-2" } as any, account: { id: "target-account-id", name: "Account-2" } as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -142,14 +142,14 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: { subscriptionId: "test-sub" } as any, subscriptionId: "test-sub",
account: { name: "test-account" } as any, account: { name: "test-account" } as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { target: {
subscriptionId: "", subscription: { subscriptionId: "test-sub" } as any,
account: null as any, account: { name: "test-account" } as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -171,14 +171,14 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null as any, subscriptionId: "",
account: { name: "test-account" } as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { target: {
subscriptionId: "", subscription: null as any,
account: null as any, account: { name: "test-account" } as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -210,13 +210,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null as any, subscriptionId: "",
account: null as any, account: null as any,
databaseId: "source-db", databaseId: "source-db",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "", subscription: null as any,
account: null as any, account: null as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -240,13 +240,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null as any, subscriptionId: "",
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "source-container", containerId: "source-container",
}, },
target: { target: {
subscriptionId: "", subscription: null as any,
account: null as any, account: null as any,
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
@@ -288,13 +288,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "valid-job-name_123", jobName: "valid-job-name_123",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null as any, subscriptionId: "",
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { target: {
subscriptionId: "", subscription: null as any,
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
@@ -318,13 +318,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "invalid job name with spaces!", jobName: "invalid job name with spaces!",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null as any, subscriptionId: "",
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { target: {
subscriptionId: "", subscription: null as any,
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
@@ -348,13 +348,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null as any, subscriptionId: "",
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { target: {
subscriptionId: "", subscription: null as any,
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
@@ -36,7 +36,7 @@ function useCreateCopyJobScreensList(goBack: () => void): Screen[] {
component: <SelectAccount />, component: <SelectAccount />,
validations: [ validations: [
{ {
validate: (state: CopyJobContextState) => !!state?.source?.subscription && !!state?.source?.account, validate: (state: CopyJobContextState) => !!state?.target?.subscription && !!state?.target?.account,
message: "Please select a subscription and account to proceed", message: "Please select a subscription and account to proceed",
}, },
], ],
@@ -20,28 +20,6 @@ jest.mock("../../../Controls/Dialog", () => ({
}, },
})); }));
jest.mock("../../ContainerCopyMessages", () => ({
__esModule: true,
default: {
MonitorJobs: {
Columns: {
actions: "Actions",
},
Actions: {
pause: "Pause",
resume: "Resume",
cancel: "Cancel",
complete: "Complete",
},
dialog: {
heading: "Confirm Action",
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
},
},
},
}));
describe("CopyJobActionMenu", () => { describe("CopyJobActionMenu", () => {
const createMockJob = (overrides: Partial<CopyJobType> = {}): CopyJobType => const createMockJob = (overrides: Partial<CopyJobType> = {}): CopyJobType =>
({ ({
@@ -301,8 +279,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(cancelButton); fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", "",
null, "",
"Confirm", "Confirm",
expect.any(Function), expect.any(Function),
"Cancel", "Cancel",
@@ -358,8 +336,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(completeButton); fireEvent.click(completeButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", "",
null, "",
"Confirm", "Confirm",
expect.any(Function), expect.any(Function),
"Cancel", "Cancel",
@@ -402,8 +380,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(cancelButton); fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", "",
null, "",
"Confirm", "Confirm",
expect.any(Function), expect.any(Function),
"Cancel", "Cancel",
@@ -433,8 +411,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(completeButton); fireEvent.click(completeButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", "",
null, "",
"Confirm", "Confirm",
expect.any(Function), expect.any(Function),
"Cancel", "Cancel",
@@ -849,8 +827,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(cancelButton); fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", // title "", // title
null, // subText "", // subText
"Confirm", // confirmLabel "Confirm", // confirmLabel
expect.any(Function), // onOk expect.any(Function), // onOk
"Cancel", // cancelLabel "Cancel", // cancelLabel
@@ -1,7 +1,7 @@
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react"; import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { useDialog } from "../../../Controls/Dialog"; import { useDialog } from "../../../Controls/Dialog";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
@@ -49,11 +49,11 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
useDialog useDialog
.getState() .getState()
.showOkCancelModalDialog( .showOkCancelModalDialog(
ContainerCopyMessages.MonitorJobs.dialog.heading, "",
null, "",
ContainerCopyMessages.MonitorJobs.dialog.confirmButtonText, t(Keys.common.confirm),
() => handleClick(job, action, setUpdatingJobAction), () => handleClick(job, action, setUpdatingJobAction),
ContainerCopyMessages.MonitorJobs.dialog.cancelButtonText, t(Keys.common.cancel),
null, null,
action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null, action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null,
); );
@@ -65,21 +65,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
const baseItems = [ const baseItems = [
{ {
key: CopyJobActions.pause, key: CopyJobActions.pause,
text: ContainerCopyMessages.MonitorJobs.Actions.pause, text: t(Keys.containerCopy.monitorJobs.actions.pause),
iconProps: { iconName: "Pause" }, iconProps: { iconName: "Pause" },
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction), onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
disabled: isThisJobUpdating, disabled: isThisJobUpdating,
}, },
{ {
key: CopyJobActions.cancel, key: CopyJobActions.cancel,
text: ContainerCopyMessages.MonitorJobs.Actions.cancel, text: t(Keys.common.cancel),
iconProps: { iconName: "Cancel" }, iconProps: { iconName: "Cancel" },
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel), onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
disabled: isThisJobUpdating, disabled: isThisJobUpdating,
}, },
{ {
key: CopyJobActions.resume, key: CopyJobActions.resume,
text: ContainerCopyMessages.MonitorJobs.Actions.resume, text: t(Keys.containerCopy.monitorJobs.actions.resume),
iconProps: { iconName: "Play" }, iconProps: { iconName: "Play" },
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction), onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
disabled: isThisJobUpdating, disabled: isThisJobUpdating,
@@ -101,7 +101,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
if ((job.Mode ?? "").toLowerCase() === CopyJobMigrationType.Online) { if ((job.Mode ?? "").toLowerCase() === CopyJobMigrationType.Online) {
filteredItems.push({ filteredItems.push({
key: CopyJobActions.complete, key: CopyJobActions.complete,
text: ContainerCopyMessages.MonitorJobs.Actions.complete, text: t(Keys.containerCopy.monitorJobs.actions.complete),
iconProps: { iconName: "CheckMark" }, iconProps: { iconName: "CheckMark" },
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete), onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
disabled: isThisJobUpdating, disabled: isThisJobUpdating,
@@ -124,8 +124,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }} iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }} menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
menuIconProps={{ iconName: "", className: "hidden" }} menuIconProps={{ iconName: "", className: "hidden" }}
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions} ariaLabel={t(Keys.containerCopy.monitorJobs.columns.actions)}
title={ContainerCopyMessages.MonitorJobs.Columns.actions} title={t(Keys.containerCopy.monitorJobs.columns.actions)}
/> />
); );
}; };
@@ -1,8 +1,8 @@
import { IColumn } from "@fluentui/react"; import { IColumn } from "@fluentui/react";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
import { getColumns } from "./CopyJobColumns"; import { getColumns } from "./CopyJobColumns";
@@ -79,14 +79,14 @@ describe("CopyJobColumns", () => {
expect(actualKeys).toEqual(expectedKeys); expect(actualKeys).toEqual(expectedKeys);
}); });
it("should have correct column names from ContainerCopyMessages", () => { it("should have correct column names", () => {
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false); const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
expect(columns[0].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime); expect(columns[0].name).toBe(t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime));
expect(columns[1].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.name); expect(columns[1].name).toBe(t(Keys.containerCopy.monitorJobs.columns.name));
expect(columns[2].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.mode); expect(columns[2].name).toBe(t(Keys.containerCopy.monitorJobs.columns.mode));
expect(columns[3].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.completionPercentage); expect(columns[3].name).toBe(t(Keys.containerCopy.monitorJobs.columns.completionPercentage));
expect(columns[4].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.status); expect(columns[4].name).toBe(t(Keys.containerCopy.monitorJobs.columns.status));
expect(columns[5].name).toBe(""); expect(columns[5].name).toBe("");
}); });
@@ -1,6 +1,6 @@
import { IColumn } from "@fluentui/react"; import { IColumn } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
import CopyJobActionMenu from "./CopyJobActionMenu"; import CopyJobActionMenu from "./CopyJobActionMenu";
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon"; import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
@@ -13,7 +13,7 @@ export const getColumns = (
): IColumn[] => [ ): IColumn[] => [
{ {
key: "LastUpdatedTime", key: "LastUpdatedTime",
name: ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime, name: t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime),
fieldName: "LastUpdatedTime", fieldName: "LastUpdatedTime",
minWidth: 140, minWidth: 140,
maxWidth: 300, maxWidth: 300,
@@ -24,7 +24,7 @@ export const getColumns = (
}, },
{ {
key: "Name", key: "Name",
name: ContainerCopyMessages.MonitorJobs.Columns.name, name: t(Keys.containerCopy.monitorJobs.columns.name),
fieldName: "Name", fieldName: "Name",
minWidth: 140, minWidth: 140,
maxWidth: 300, maxWidth: 300,
@@ -36,7 +36,7 @@ export const getColumns = (
}, },
{ {
key: "Mode", key: "Mode",
name: ContainerCopyMessages.MonitorJobs.Columns.mode, name: t(Keys.containerCopy.monitorJobs.columns.mode),
fieldName: "Mode", fieldName: "Mode",
minWidth: 90, minWidth: 90,
maxWidth: 200, maxWidth: 200,
@@ -47,7 +47,7 @@ export const getColumns = (
}, },
{ {
key: "CompletionPercentage", key: "CompletionPercentage",
name: ContainerCopyMessages.MonitorJobs.Columns.completionPercentage, name: t(Keys.containerCopy.monitorJobs.columns.completionPercentage),
fieldName: "CompletionPercentage", fieldName: "CompletionPercentage",
minWidth: 110, minWidth: 110,
maxWidth: 200, maxWidth: 200,
@@ -59,7 +59,7 @@ export const getColumns = (
}, },
{ {
key: "CopyJobStatus", key: "CopyJobStatus",
name: ContainerCopyMessages.MonitorJobs.Columns.status, name: t(Keys.containerCopy.monitorJobs.columns.status),
fieldName: "Status", fieldName: "Status",
minWidth: 130, minWidth: 130,
maxWidth: 200, maxWidth: 200,
@@ -13,22 +13,6 @@ jest.mock("./CopyJobStatusWithIcon", () => {
return MockCopyJobStatusWithIcon; return MockCopyJobStatusWithIcon;
}); });
jest.mock("../../ContainerCopyMessages", () => ({
errorTitle: "Error Details",
sourceDatabaseLabel: "Source Database",
sourceContainerLabel: "Source Container",
targetDatabaseLabel: "Destination Database",
targetContainerLabel: "Destination Container",
sourceAccountLabel: "Source Account",
MonitorJobs: {
Columns: {
lastUpdatedTime: "Date & time",
status: "Status",
mode: "Mode",
},
},
}));
describe("CopyJobDetails", () => { describe("CopyJobDetails", () => {
const mockBasicJob: CopyJobType = { const mockBasicJob: CopyJobType = {
ID: "test-job-1", ID: "test-job-1",
@@ -102,8 +86,8 @@ describe("CopyJobDetails", () => {
expect(screen.getByText("Date & time")).toBeInTheDocument(); expect(screen.getByText("Date & time")).toBeInTheDocument();
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument(); expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
expect(screen.getByText("Source Account")).toBeInTheDocument(); expect(screen.getByText("Destination account")).toBeInTheDocument();
expect(screen.getByText("sourceAccount")).toBeInTheDocument(); expect(screen.getByText("targetAccount")).toBeInTheDocument();
expect(screen.getByText("Mode")).toBeInTheDocument(); expect(screen.getByText("Mode")).toBeInTheDocument();
expect(screen.getByText("Offline")).toBeInTheDocument(); expect(screen.getByText("Offline")).toBeInTheDocument();
@@ -263,7 +247,7 @@ describe("CopyJobDetails", () => {
expect(screen.getByText("complex_source_container_with_underscores")).toBeInTheDocument(); expect(screen.getByText("complex_source_container_with_underscores")).toBeInTheDocument();
expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument(); expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument();
expect(screen.getByText("complex_target_container_with_underscores")).toBeInTheDocument(); expect(screen.getByText("complex_target_container_with_underscores")).toBeInTheDocument();
expect(screen.getByText("complex.source.account")).toBeInTheDocument(); expect(screen.getByText("complex.target.account")).toBeInTheDocument();
}); });
}); });
@@ -322,11 +306,11 @@ describe("CopyJobDetails", () => {
render(<CopyJobDetails job={mockBasicJob} />); render(<CopyJobDetails job={mockBasicJob} />);
const dateTimeHeading = screen.getByText("Date & time"); const dateTimeHeading = screen.getByText("Date & time");
const sourceAccountHeading = screen.getByText("Source Account"); const destinationAccountHeading = screen.getByText("Destination account");
const modeHeading = screen.getByText("Mode"); const modeHeading = screen.getByText("Mode");
expect(dateTimeHeading).toHaveClass("bold"); expect(dateTimeHeading).toHaveClass("bold");
expect(sourceAccountHeading).toHaveClass("bold"); expect(destinationAccountHeading).toHaveClass("bold");
expect(modeHeading).toHaveClass("bold"); expect(modeHeading).toHaveClass("bold");
}); });
}); });
@@ -1,7 +1,7 @@
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react"; import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import React, { memo } from "react"; import React, { memo } from "react";
import { useThemeStore } from "../../../../hooks/useTheme"; import { useThemeStore } from "../../../../hooks/useTheme";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType } from "../../Types/CopyJobTypes"; import { CopyJobType } from "../../Types/CopyJobTypes";
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon"; import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
@@ -31,31 +31,31 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
return [ return [
{ {
key: "sourcedbcol", key: "sourcedbcol",
name: ContainerCopyMessages.sourceDatabaseLabel, name: t(Keys.containerCopy.preview.sourceDatabaseLabel),
fieldName: "sourceDatabaseName", fieldName: "sourceDatabaseName",
...commonProps, ...commonProps,
}, },
{ {
key: "sourcecol", key: "sourcecol",
name: ContainerCopyMessages.sourceContainerLabel, name: t(Keys.containerCopy.preview.sourceContainerLabel),
fieldName: "sourceContainerName", fieldName: "sourceContainerName",
...commonProps, ...commonProps,
}, },
{ {
key: "targetdbcol", key: "targetdbcol",
name: ContainerCopyMessages.targetDatabaseLabel, name: t(Keys.containerCopy.preview.targetDatabaseLabel),
fieldName: "targetDatabaseName", fieldName: "targetDatabaseName",
...commonProps, ...commonProps,
}, },
{ {
key: "targetcol", key: "targetcol",
name: ContainerCopyMessages.targetContainerLabel, name: t(Keys.containerCopy.preview.targetContainerLabel),
fieldName: "targetContainerName", fieldName: "targetContainerName",
...commonProps, ...commonProps,
}, },
{ {
key: "statuscol", key: "statuscol",
name: ContainerCopyMessages.MonitorJobs.Columns.status, name: t(Keys.containerCopy.monitorJobs.columns.status),
fieldName: "jobStatus", fieldName: "jobStatus",
onRender: ({ jobStatus }: { jobStatus: CopyJobStatusType }) => <CopyJobStatusWithIcon status={jobStatus} />, onRender: ({ jobStatus }: { jobStatus: CopyJobStatusType }) => <CopyJobStatusWithIcon status={jobStatus} />,
...commonProps, ...commonProps,
@@ -92,7 +92,7 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
{job.Error ? ( {job.Error ? (
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}> <Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
<Text className="bold themeText" style={sectionCss.headingText}> <Text className="bold themeText" style={sectionCss.headingText}>
{ContainerCopyMessages.errorTitle} {t(Keys.containerCopy.jobDetails.errorTitle)}
</Text> </Text>
<Text as="pre" style={errorMessageStyle}> <Text as="pre" style={errorMessageStyle}>
{job.Error.message} {job.Error.message}
@@ -102,15 +102,15 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
<Stack.Item data-testid="selectedcollection-stack"> <Stack.Item data-testid="selectedcollection-stack">
<Stack tokens={{ childrenGap: 15 }}> <Stack tokens={{ childrenGap: 15 }}>
<Stack.Item style={sectionCss.verticalAlign}> <Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text> <Text className="bold themeText">{t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime)}</Text>
<Text className="themeText">{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 themeText">{ContainerCopyMessages.sourceAccountLabel}</Text> <Text className="bold themeText">{t(Keys.containerCopy.preview.accountLabel)}</Text>
<Text className="themeText">{job.Source?.remoteAccountName}</Text> <Text className="themeText">{job.Destination?.remoteAccountName}</Text>
</Stack.Item> </Stack.Item>
<Stack.Item style={sectionCss.verticalAlign}> <Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text> <Text className="bold themeText">{t(Keys.containerCopy.monitorJobs.columns.mode)}</Text>
<Text className="themeText">{job.Mode}</Text> <Text className="themeText">{job.Mode}</Text>
</Stack.Item> </Stack.Item>
</Stack> </Stack>
@@ -1,7 +1,7 @@
import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react"; import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
const iconClass = mergeStyles({ const iconClass = mergeStyles({
@@ -30,12 +30,25 @@ const statusIconColors: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Paused]: "var(--colorBrandForeground1)", [CopyJobStatusType.Paused]: "var(--colorBrandForeground1)",
}; };
const statusKeyMap: Record<CopyJobStatusType, string> = {
[CopyJobStatusType.Pending]: Keys.containerCopy.monitorJobs.status.pending,
[CopyJobStatusType.InProgress]: Keys.containerCopy.monitorJobs.status.inProgress,
[CopyJobStatusType.Running]: Keys.containerCopy.monitorJobs.status.running,
[CopyJobStatusType.Partitioning]: Keys.containerCopy.monitorJobs.status.partitioning,
[CopyJobStatusType.Paused]: Keys.containerCopy.monitorJobs.status.paused,
[CopyJobStatusType.Completed]: Keys.containerCopy.monitorJobs.status.completed,
[CopyJobStatusType.Failed]: Keys.containerCopy.monitorJobs.status.failed,
[CopyJobStatusType.Faulted]: Keys.containerCopy.monitorJobs.status.faulted,
[CopyJobStatusType.Skipped]: Keys.containerCopy.monitorJobs.status.skipped,
[CopyJobStatusType.Cancelled]: Keys.containerCopy.monitorJobs.status.cancelled,
};
export interface CopyJobStatusWithIconProps { export interface CopyJobStatusWithIconProps {
status: CopyJobStatusType; status: CopyJobStatusType;
} }
const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo(({ status }) => { const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo(({ status }) => {
const statusText = ContainerCopyMessages.MonitorJobs.Status[status] || "Unknown"; const statusText = statusKeyMap[status] ? t(statusKeyMap[status] as Parameters<typeof t>[0]) : "Unknown";
const isSpinnerStatus = [ const isSpinnerStatus = [
CopyJobStatusType.Running, CopyJobStatusType.Running,
@@ -3,9 +3,9 @@ jest.mock("../../Actions/CopyJobActions");
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import * as Actions from "../../Actions/CopyJobActions"; import * as Actions from "../../Actions/CopyJobActions";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import CopyJobsNotFound from "./CopyJobs.NotFound"; import CopyJobsNotFound from "./CopyJobs.NotFound";
describe("CopyJobsNotFound", () => { describe("CopyJobsNotFound", () => {
@@ -22,10 +22,10 @@ describe("CopyJobsNotFound", () => {
const image = container.querySelector(".notFoundContainer .ms-Image"); const image = container.querySelector(".notFoundContainer .ms-Image");
expect(image).toBeInTheDocument(); expect(image).toBeInTheDocument();
expect(image).toHaveAttribute("style", "width: 100px; height: 100px;"); expect(image).toHaveAttribute("style", "width: 100px; height: 100px;");
expect(getByText(ContainerCopyMessages.noCopyJobsTitle)).toBeInTheDocument(); expect(getByText(t(Keys.containerCopy.noCopyJobs.title))).toBeInTheDocument();
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.createCopyJobButtonText, name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
expect(button).toHaveClass("createCopyJobButton"); expect(button).toHaveClass("createCopyJobButton");
@@ -45,7 +45,7 @@ describe("CopyJobsNotFound", () => {
render(<CopyJobsNotFound explorer={mockExplorer} />); render(<CopyJobsNotFound explorer={mockExplorer} />);
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.createCopyJobButtonText, name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
}); });
fireEvent.click(button); fireEvent.click(button);
@@ -58,11 +58,11 @@ describe("CopyJobsNotFound", () => {
render(<CopyJobsNotFound explorer={mockExplorer} />); render(<CopyJobsNotFound explorer={mockExplorer} />);
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.createCopyJobButtonText, name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
expect(button.textContent).toBe(ContainerCopyMessages.createCopyJobButtonText); expect(button.textContent).toBe(t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText));
}); });
it("should use memo to prevent unnecessary re-renders", () => { it("should use memo to prevent unnecessary re-renders", () => {
@@ -1,9 +1,9 @@
import { ActionButton, Image } from "@fluentui/react"; import { ActionButton, Image } from "@fluentui/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg"; import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg";
import * as Actions from "../../Actions/CopyJobActions"; import * as Actions from "../../Actions/CopyJobActions";
import ContainerCopyMessages from "../../ContainerCopyMessages";
interface CopyJobsNotFoundProps { interface CopyJobsNotFoundProps {
explorer: Explorer; explorer: Explorer;
@@ -12,14 +12,14 @@ interface CopyJobsNotFoundProps {
const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => { const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => {
return ( return (
<div className="notFoundContainer flexContainer centerContent"> <div className="notFoundContainer flexContainer centerContent">
<Image src={CopyJobIcon} alt={ContainerCopyMessages.noCopyJobsTitle} width={100} height={100} /> <Image src={CopyJobIcon} alt={t(Keys.containerCopy.noCopyJobs.title)} width={100} height={100} />
<h4 className="noCopyJobsMessage">{ContainerCopyMessages.noCopyJobsTitle}</h4> <h4 className="noCopyJobsMessage">{t(Keys.containerCopy.noCopyJobs.title)}</h4>
<ActionButton <ActionButton
allowDisabledFocus allowDisabledFocus
className="createCopyJobButton" className="createCopyJobButton"
onClick={() => Actions.openCreateCopyJobPanel(explorer)} onClick={() => Actions.openCreateCopyJobPanel(explorer)}
> >
{ContainerCopyMessages.createCopyJobButtonText} {t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText)}
</ActionButton> </ActionButton>
</div> </div>
); );
@@ -55,15 +55,15 @@ export interface DatabaseContainerSectionProps {
export interface CopyJobContextState { export interface CopyJobContextState {
jobName: string; jobName: string;
migrationType: CopyJobMigrationType; migrationType: CopyJobMigrationType;
sourceReadAccessFromTarget?: boolean; sourceReadWriteAccessFromTarget?: boolean;
source: { source: {
subscription: Subscription | null; subscriptionId: string;
account: DatabaseAccount | null; account: DatabaseAccount | null;
databaseId: string; databaseId: string;
containerId: string; containerId: string;
}; };
target: { target: {
subscriptionId: string; subscription: Subscription | null;
account: DatabaseAccount | null; account: DatabaseAccount | null;
databaseId: string; databaseId: string;
containerId: string; containerId: string;
@@ -10,7 +10,7 @@ import {
Stack, Stack,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels"; import { AccountOverride, FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import * as React from "react"; import * as React from "react";
import { isFullTextSearchPreviewFeaturesEnabled } from "Utils/CapabilityUtils"; import { isFullTextSearchPreviewFeaturesEnabled } from "Utils/CapabilityUtils";
@@ -25,6 +25,7 @@ export interface FullTextPoliciesComponentProps {
discardChanges?: boolean; discardChanges?: boolean;
onChangesDiscarded?: () => void; onChangesDiscarded?: () => void;
englishOnly?: boolean; englishOnly?: boolean;
targetAccountOverride?: AccountOverride;
} }
export interface FullTextPolicyData { export interface FullTextPolicyData {
@@ -206,6 +207,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
discardChanges, discardChanges,
onChangesDiscarded, onChangesDiscarded,
englishOnly, englishOnly,
targetAccountOverride,
}): JSX.Element => { }): JSX.Element => {
const getFullTextPathError = (path: string, index?: number): string => { const getFullTextPathError = (path: string, index?: number): string => {
let error = ""; let error = "";
@@ -236,7 +238,9 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy)); const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy));
const [defaultLanguage, setDefaultLanguage] = React.useState<string>( const [defaultLanguage, setDefaultLanguage] = React.useState<string>(
fullTextPolicy ? fullTextPolicy.defaultLanguage : (getFullTextLanguageOptions()[0].key as never), fullTextPolicy
? fullTextPolicy.defaultLanguage
: (getFullTextLanguageOptions(englishOnly, targetAccountOverride)[0].key as never),
); );
React.useEffect(() => { React.useEffect(() => {
@@ -307,7 +311,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
<Dropdown <Dropdown
required={true} required={true}
styles={dropdownStyles} styles={dropdownStyles}
options={getFullTextLanguageOptions(englishOnly)} options={getFullTextLanguageOptions(englishOnly, targetAccountOverride)}
selectedKey={defaultLanguage} selectedKey={defaultLanguage}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) => onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
setDefaultLanguage(option.key as never) setDefaultLanguage(option.key as never)
@@ -352,7 +356,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
<Dropdown <Dropdown
required={true} required={true}
styles={dropdownStyles} styles={dropdownStyles}
options={getFullTextLanguageOptions(englishOnly)} options={getFullTextLanguageOptions(englishOnly, targetAccountOverride)}
selectedKey={fullTextPolicy.language} selectedKey={fullTextPolicy.language}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) => onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onFullTextPathPolicyChange(index, option) onFullTextPathPolicyChange(index, option)
@@ -395,8 +399,12 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
); );
}; };
export const getFullTextLanguageOptions = (englishOnly?: boolean): IDropdownOption[] => { export const getFullTextLanguageOptions = (
const multiLanguageSupportEnabled: boolean = isFullTextSearchPreviewFeaturesEnabled() && !englishOnly; englishOnly?: boolean,
targetAccountOverride?: AccountOverride,
): IDropdownOption[] => {
const multiLanguageSupportEnabled: boolean =
isFullTextSearchPreviewFeaturesEnabled(targetAccountOverride) && !englishOnly;
const fullTextLanguageOptions: IDropdownOption[] = [ const fullTextLanguageOptions: IDropdownOption[] = [
{ {
key: "en-US", key: "en-US",
+169
View File
@@ -0,0 +1,169 @@
jest.mock("Utils/arm/generatedClients/cosmos/databaseAccounts");
jest.mock("Utils/NotificationConsoleUtils", () => ({
logConsoleProgress: jest.fn(() => jest.fn()), // returns a clearMessage fn
logConsoleInfo: jest.fn(),
logConsoleError: jest.fn(),
}));
jest.mock("Shared/Telemetry/TelemetryProcessor");
import { Capability, DatabaseAccount } from "../Contracts/DataModels";
import { updateUserContext, userContext } from "../UserContext";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import Explorer from "./Explorer";
const mockUpdate = update as jest.MockedFunction<typeof update>;
// Capture `useDialog.getState().openDialog` calls
const mockOpenDialog = jest.fn();
const mockCloseDialog = jest.fn();
jest.mock("./Controls/Dialog", () => ({
useDialog: {
getState: jest.fn(() => ({
openDialog: mockOpenDialog,
closeDialog: mockCloseDialog,
})),
},
}));
// Silence useNotebook subscription calls
jest.mock("./Notebook/useNotebook", () => ({
useNotebook: {
subscribe: jest.fn(),
getState: jest.fn().mockReturnValue(
new Proxy(
{},
{
get: () => jest.fn().mockResolvedValue(undefined),
},
),
),
},
}));
describe("Explorer.openEnableSynapseLinkDialog", () => {
let explorer: Explorer;
const baseAccount: DatabaseAccount = {
id: "/subscriptions/ctx-sub/resourceGroups/ctx-rg/providers/Microsoft.DocumentDB/databaseAccounts/ctx-account",
name: "ctx-account",
location: "East US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
tags: {},
properties: {
documentEndpoint: "https://ctx-account.documents.azure.com:443/",
capabilities: [] as Capability[],
enableMultipleWriteLocations: false,
},
};
beforeAll(() => {
updateUserContext({
databaseAccount: baseAccount,
subscriptionId: "ctx-sub",
resourceGroup: "ctx-rg",
});
});
beforeEach(() => {
jest.clearAllMocks();
mockUpdate.mockResolvedValue(undefined);
explorer = new Explorer();
});
describe("without targetAccountOverride", () => {
it("should open a dialog when called without override", () => {
explorer.openEnableSynapseLinkDialog();
expect(mockOpenDialog).toHaveBeenCalledTimes(1);
});
it("should use userContext values in the update call on primary button click", async () => {
explorer.openEnableSynapseLinkDialog();
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(mockUpdate).toHaveBeenCalledWith(
"ctx-sub",
"ctx-rg",
"ctx-account",
expect.objectContaining({
properties: { enableAnalyticalStorage: true },
}),
);
});
it("should update userContext.databaseAccount.properties when no override is provided", async () => {
explorer.openEnableSynapseLinkDialog();
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(userContext.databaseAccount.properties.enableAnalyticalStorage).toBe(true);
});
});
describe("with targetAccountOverride", () => {
const override = {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
capabilities: [] as Capability[],
};
it("should open a dialog when called with override", () => {
explorer.openEnableSynapseLinkDialog(override);
expect(mockOpenDialog).toHaveBeenCalledTimes(1);
});
it("should use override values in the update call on primary button click", async () => {
explorer.openEnableSynapseLinkDialog(override);
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(mockUpdate).toHaveBeenCalledWith(
"override-sub",
"override-rg",
"override-account",
expect.objectContaining({
properties: { enableAnalyticalStorage: true },
}),
);
});
it("should NOT update userContext.databaseAccount.properties when override is provided", async () => {
// Reset the property first
userContext.databaseAccount.properties.enableAnalyticalStorage = false;
explorer.openEnableSynapseLinkDialog(override);
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(userContext.databaseAccount.properties.enableAnalyticalStorage).toBe(false);
});
it("should use override values — NOT userContext — even when userContext has different values", async () => {
explorer.openEnableSynapseLinkDialog(override);
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
// update should NOT be called with ctx-sub / ctx-rg / ctx-account
expect(mockUpdate).not.toHaveBeenCalledWith("ctx-sub", expect.anything(), expect.anything(), expect.anything());
});
});
describe("secondary button click", () => {
it("should close the dialog on secondary button click", () => {
explorer.openEnableSynapseLinkDialog();
const dialogProps = mockOpenDialog.mock.calls[0][0];
dialogProps.onSecondaryButtonClick();
expect(mockCloseDialog).toHaveBeenCalledTimes(1);
});
});
});
+9 -3
View File
@@ -223,7 +223,11 @@ export default class Explorer {
this.refreshNotebookList(); this.refreshNotebookList();
} }
public openEnableSynapseLinkDialog(): void { public openEnableSynapseLinkDialog(targetAccountOverride?: DataModels.AccountOverride): void {
const subscriptionId = targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
const resourceGroup = targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
const accountName = targetAccountOverride?.accountName ?? userContext.databaseAccount.name;
const addSynapseLinkDialogProps: DialogProps = { const addSynapseLinkDialogProps: DialogProps = {
linkProps: { linkProps: {
linkText: "Learn more", linkText: "Learn more",
@@ -245,7 +249,7 @@ export default class Explorer {
useDialog.getState().closeDialog(); useDialog.getState().closeDialog();
try { try {
await update(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, { await update(subscriptionId, resourceGroup, accountName, {
properties: { properties: {
enableAnalyticalStorage: true, enableAnalyticalStorage: true,
}, },
@@ -254,7 +258,9 @@ export default class Explorer {
clearInProgressMessage(); clearInProgressMessage();
logConsoleInfo("Enabled Azure Synapse Link for this account"); logConsoleInfo("Enabled Azure Synapse Link for this account");
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime); TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
userContext.databaseAccount.properties.enableAnalyticalStorage = true; if (!targetAccountOverride) {
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
}
} catch (error) { } catch (error) {
clearInProgressMessage(); clearInProgressMessage();
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`); logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
@@ -1,3 +1,4 @@
import { Capability } from "Contracts/DataModels";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -12,4 +13,58 @@ describe("AddCollectionPanel", () => {
const wrapper = shallow(<AddCollectionPanel {...props} />); const wrapper = shallow(<AddCollectionPanel {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
describe("targetAccountOverride prop", () => {
it("should render with targetAccountOverride prop set", () => {
const override = {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
capabilities: [] as Capability[],
};
const wrapper = shallow(<AddCollectionPanel {...props} targetAccountOverride={override} />);
expect(wrapper).toBeDefined();
});
it("should pass targetAccountOverride to openEnableSynapseLinkDialog button click", () => {
const mockOpenEnableSynapseLinkDialog = jest.fn();
const explorerWithMock = { ...props.explorer, openEnableSynapseLinkDialog: mockOpenEnableSynapseLinkDialog };
const override = {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
capabilities: [] as Capability[],
};
const wrapper = shallow(
<AddCollectionPanel explorer={explorerWithMock as unknown as Explorer} targetAccountOverride={override} />,
);
// isSynapseLinkEnabled section requires specific conditions; verify the component exists
expect(wrapper).toBeDefined();
});
});
describe("externalDatabaseOptions prop", () => {
it("should accept externalDatabaseOptions without error", () => {
const externalOptions = [
{ key: "db1", text: "Database One" },
{ key: "db2", text: "Database Two" },
];
const wrapper = shallow(<AddCollectionPanel {...props} externalDatabaseOptions={externalOptions} />);
expect(wrapper).toBeDefined();
});
});
describe("isCopyJobFlow prop", () => {
it("should render with isCopyJobFlow=true", () => {
const wrapper = shallow(<AddCollectionPanel {...props} isCopyJobFlow={true} />);
expect(wrapper).toBeDefined();
});
it("should render with isCopyJobFlow=false (default behaviour)", () => {
const wrapper = shallow(<AddCollectionPanel {...props} isCopyJobFlow={false} />);
expect(wrapper).toBeDefined();
});
});
}); });
@@ -20,6 +20,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { AccountOverride } from "Contracts/DataModels";
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { import {
@@ -67,6 +68,8 @@ export interface AddCollectionPanelProps {
isQuickstart?: boolean; isQuickstart?: boolean;
isCopyJobFlow?: boolean; isCopyJobFlow?: boolean;
onSubmitSuccess?: (collectionData: { databaseId: string; collectionId: string }) => void; onSubmitSuccess?: (collectionData: { databaseId: string; collectionId: string }) => void;
targetAccountOverride?: AccountOverride;
externalDatabaseOptions?: IDropdownOption[];
} }
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = { export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
@@ -167,7 +170,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
)} )}
{!this.state.errorMessage && isFreeTierAccount() && ( {!this.state.errorMessage && isFreeTierAccount(this.props.targetAccountOverride) && (
<PanelInfoErrorComponent <PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)} message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info" messageType="info"
@@ -644,53 +647,57 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
)} )}
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && ( {!isServerlessAccount(this.props.targetAccountOverride) &&
<Stack horizontal verticalAlign="center"> !this.state.createNewDatabase &&
<Checkbox this.isSelectedDatabaseSharedThroughput() && (
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, { <Stack horizontal verticalAlign="center">
collectionName: getCollectionName().toLocaleLowerCase(), <Checkbox
})} label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
checked={this.state.enableDedicatedThroughput} collectionName: getCollectionName().toLocaleLowerCase(),
styles={{ })}
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" }, checked={this.state.enableDedicatedThroughput}
checkbox: { width: 12, height: 12 }, styles={{
label: { padding: 0, alignItems: "center" }, text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
root: { checkbox: { width: 12, height: 12 },
selectors: { label: { padding: 0, alignItems: "center" },
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" }, root: {
selectors: {
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" },
},
}, },
}, }}
}} onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => this.setState({ enableDedicatedThroughput: isChecked })
this.setState({ enableDedicatedThroughput: isChecked }) }
} />
/> <TooltipHost
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge}
directionalHint={DirectionalHint.bottomLeftEdge} content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(), collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(), collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})} })}
/> >
</TooltipHost> <Icon
</Stack> iconName="Info"
)} className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
/>
</TooltipHost>
</Stack>
)}
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && ( {this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={
isFreeTierAccount(this.props.targetAccountOverride) && !isFirstResourceCreated
}
isDatabase={false} isDatabase={false}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={isFreeTierAccount()} isFreeTier={isFreeTierAccount(this.props.targetAccountOverride)}
isQuickstart={this.props.isQuickstart} isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)} setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
@@ -767,7 +774,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={this.state.enableAnalyticalStore} checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()} disabled={!isSynapseLinkEnabled(this.props.targetAccountOverride)}
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)} aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
aria-checked={this.state.enableAnalyticalStore} aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
@@ -782,7 +789,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore} checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()} disabled={!isSynapseLinkEnabled(this.props.targetAccountOverride)}
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)} aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
aria-checked={!this.state.enableAnalyticalStore} aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
@@ -796,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div> </div>
</Stack> </Stack>
{!isSynapseLinkEnabled() && ( {!isSynapseLinkEnabled(this.props.targetAccountOverride) && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, { {t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
@@ -814,7 +821,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text> </Text>
<DefaultButton <DefaultButton
text={t(Keys.panes.addCollection.enable)} text={t(Keys.panes.addCollection.enable)}
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()} onClick={() => this.props.explorer.openEnableSynapseLinkDialog(this.props.targetAccountOverride)}
style={{ height: 27, width: 80 }} style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }} styles={{ label: { fontSize: 12 } }}
/> />
@@ -865,6 +872,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}> <Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}> <Stack styles={{ root: { paddingLeft: 40 } }}>
<FullTextPoliciesComponent <FullTextPoliciesComponent
targetAccountOverride={this.props.targetAccountOverride}
fullTextPolicy={this.state.fullTextPolicy} fullTextPolicy={this.state.fullTextPolicy}
onFullTextPathChange={( onFullTextPathChange={(
fullTextPolicy: DataModels.FullTextPolicy, fullTextPolicy: DataModels.FullTextPolicy,
@@ -1000,6 +1008,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private getDatabaseOptions(): IDropdownOption[] { private getDatabaseOptions(): IDropdownOption[] {
if (this.props.externalDatabaseOptions) {
return this.props.externalDatabaseOptions;
}
return useDatabases.getState().databases?.map((database) => ({ return useDatabases.getState().databases?.map((database) => ({
key: database.id(), key: database.id(),
text: database.id(), text: database.id(),
@@ -1087,6 +1098,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false; return false;
} }
if (this.props.targetAccountOverride) {
return false;
}
const selectedDatabase = useDatabases const selectedDatabase = useDatabases
.getState() .getState()
.databases?.find((database) => database.id() === this.state.selectedDatabaseId); .databases?.find((database) => database.id() === this.state.selectedDatabaseId);
@@ -1124,7 +1139,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// } // }
private shouldShowCollectionThroughputInput(): boolean { private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) { if (isServerlessAccount(this.props.targetAccountOverride)) {
return false; return false;
} }
@@ -1140,7 +1155,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private shouldShowIndexingOptionsForFreeTierAccount(): boolean { private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
if (!isFreeTierAccount()) { if (!isFreeTierAccount(this.props.targetAccountOverride)) {
return false; return false;
} }
@@ -1148,7 +1163,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private shouldShowVectorSearchParameters() { private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput()); const targetAccount = this.props.targetAccountOverride;
return (
isVectorSearchEnabled(targetAccount) &&
(isServerlessAccount(targetAccount) || this.shouldShowCollectionThroughputInput())
);
} }
private shouldShowFullTextSearchParameters() { private shouldShowFullTextSearchParameters() {
@@ -1227,7 +1246,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private getAnalyticalStorageTtl(): number { private getAnalyticalStorageTtl(): number {
if (!isSynapseLinkEnabled()) { if (!isSynapseLinkEnabled(this.props.targetAccountOverride)) {
return undefined; return undefined;
} }
@@ -1367,13 +1386,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
createMongoWildcardIndex: this.state.createMongoWildCardIndex, createMongoWildcardIndex: this.state.createMongoWildCardIndex,
vectorEmbeddingPolicy, vectorEmbeddingPolicy,
fullTextPolicy: this.state.fullTextPolicy, fullTextPolicy: this.state.fullTextPolicy,
targetAccountOverride: this.props.targetAccountOverride,
}; };
this.setState({ isExecuting: true }); this.setState({ isExecuting: true });
try { try {
await createCollection(createCollectionParams); await createCollection(createCollectionParams);
await this.props.explorer.refreshAllDatabases(); if (!this.props.isCopyJobFlow) {
await this.props.explorer.refreshAllDatabases();
}
if (this.props.isQuickstart) { if (this.props.isQuickstart) {
const database = useDatabases.getState().findDatabaseWithId(databaseId); const database = useDatabases.getState().findDatabaseWithId(databaseId);
if (database) { if (database) {
@@ -1402,7 +1424,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
} }
} catch (error) { } catch (error) {
const errorMessage: string = getErrorMessage(error); const rawMessage: string = getErrorMessage(error);
const errorMessage =
this.props.isCopyJobFlow && (rawMessage.includes("AuthorizationFailed") || rawMessage.includes("403"))
? `You do not have permission to create databases or containers on the destination account (${
this.props.targetAccountOverride?.accountName ?? "unknown"
}). Please ensure you have Contributor or Owner access.`
: rawMessage;
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true }); this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) }; const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
TelemetryProcessor.traceFailure(Action.CreateCollection, failureTelemetryData, startKey); TelemetryProcessor.traceFailure(Action.CreateCollection, failureTelemetryData, startKey);
@@ -2,6 +2,7 @@ import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { AccountOverride } from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { Keys, t } from "Localization"; import { Keys, t } from "Localization";
import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { isFabricNative } from "Platform/Fabric/FabricUtil";
@@ -68,7 +69,10 @@ export function getPartitionKey(isQuickstart?: boolean): string {
return ""; return "";
} }
export function isFreeTierAccount(): boolean { export function isFreeTierAccount(targetAccountOverride?: AccountOverride): boolean {
if (targetAccountOverride) {
return targetAccountOverride.enableFreeTier ?? false;
}
return userContext.databaseAccount?.properties?.enableFreeTier; return userContext.databaseAccount?.properties?.enableFreeTier;
} }
@@ -130,7 +134,16 @@ export function AnalyticalStorageContent(): JSX.Element {
); );
} }
export function isSynapseLinkEnabled(): boolean { export function isSynapseLinkEnabled(targetAccountOverride?: AccountOverride): boolean {
if (targetAccountOverride) {
if (targetAccountOverride.enableAnalyticalStorage) {
return true;
}
return targetAccountOverride.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
if (!userContext.databaseAccount) { if (!userContext.databaseAccount) {
return false; return false;
} }
+161
View File
@@ -34,6 +34,8 @@
"browse": "Browse", "browse": "Browse",
"increaseValueBy1": "Increase value by 1", "increaseValueBy1": "Increase value by 1",
"decreaseValueBy1": "Decrease value by 1", "decreaseValueBy1": "Decrease value by 1",
"on": "On",
"off": "Off",
"preview": "Preview" "preview": "Preview"
}, },
"splashScreen": { "splashScreen": {
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "Quantization byte size must be greater than 0 and less than or equal to 512", "quantizationByteSizeRangeError": "Quantization byte size must be greater than 0 and less than or equal to 512",
"indexingSearchListSizeRangeError": "Indexing search list size must be greater than or equal to 25 and less than or equal to 500" "indexingSearchListSizeRangeError": "Indexing search list size must be greater than or equal to 25 and less than or equal to 500"
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Feedback",
"feedbackButtonAriaLabel": "Provide feedback on copy jobs",
"refreshButtonAriaLabel": "Refresh copy jobs",
"createCopyJobButtonLabel": "Create Copy Job",
"createCopyJobButtonAriaLabel": "Create a new container copy job"
},
"noCopyJobs": {
"title": "No copy jobs to show",
"createCopyJobButtonText": "Create a container copy job"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Job Details",
"errorTitle": "Error Details",
"selectedContainers": "Selected Containers"
},
"createCopyJob": {
"panelTitle": "Create copy job"
},
"selectAccount": {
"description": "Please select a destination account to copy to.",
"subscriptionDropdownLabel": "Subscription",
"subscriptionDropdownPlaceholder": "Select a subscription",
"accountDropdownLabel": "Account",
"accountDropdownPlaceholder": "Select an account"
},
"migrationType": {
"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)."
}
},
"selectContainers": {
"description": "Please select a source container and a destination container to copy to.",
"sourceContainerSubHeading": "Source container",
"targetContainerSubHeading": "Destination container",
"databaseDropdownLabel": "Database",
"databaseDropdownPlaceholder": "Select a database",
"containerDropdownLabel": "Container",
"containerDropdownPlaceholder": "Select a container",
"createNewContainerSubHeading": "Configure the properties for the new container on destination account \"{{accountName}}\".",
"createNewContainerSubHeadingDefault": "Configure the properties for the new container.",
"createContainerButtonLabel": "Create a new container",
"createContainerHeading": "Create new container"
},
"preview": {
"jobNameLabel": "Job name",
"subscriptionLabel": "Destination subscription",
"accountLabel": "Destination account",
"sourceDatabaseLabel": "Source database",
"sourceContainerLabel": "Source container",
"targetDatabaseLabel": "Destination database",
"targetContainerLabel": "Destination container"
},
"assignPermissions": {
"crossAccountDescription": "To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.",
"intraAccountOnlineDescription": "Follow the steps below to enable online copy on your \"{{accountName}}\" account.",
"crossAccountConfiguration": {
"title": "Cross-account container copy",
"description": "Please follow the instruction below to grant requisite permissions to copy data from \"{{sourceAccount}}\" to \"{{destinationAccount}}\"."
},
"onlineConfiguration": {
"title": "Online container copy",
"description": "Please follow the instructions below to enable online copy on your \"{{accountName}}\" account."
}
},
"popoverOverlaySpinnerLabel": "Please wait while we process your request...",
"addManagedIdentity": {
"title": "System-assigned managed identity enabled.",
"description": "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.",
"descriptionHrefText": "Learn more about Managed identities.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "System assigned managed identity",
"tooltipContent": "Learn more about",
"tooltipHrefText": "Managed Identities.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "You can select an existing user assigned identity or create a new one.",
"userAssignedIdentityLabel": "You may also select a user assigned managed identity.",
"createUserAssignedIdentityLink": "Create User Assigned Managed Identity",
"enablementTitle": "Enable system assigned managed identity",
"enablementDescription": "Enable system-assigned managed identity on the {{accountName}}. To confirm, click the \"Yes\" button."
},
"defaultManagedIdentity": {
"title": "System-assigned managed identity set as default.",
"description": "Set the system-assigned managed identity as default for \"{{accountName}}\" by switching it on.",
"tooltipContent": "Learn more about",
"tooltipHrefText": "Default Managed Identities.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "System assigned managed identity set as default",
"popoverDescription": "Assign the system-assigned managed identity as the default for \"{{accountName}}\". To confirm, click the \"Yes\" button. "
},
"readWritePermissionAssigned": {
"title": "Read-write permissions assigned to the default identity.",
"description": "To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.",
"tooltipContent": "Learn more about",
"tooltipHrefText": "Read-write permissions.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Assign read-write permissions to default identity.",
"popoverDescription": "Assign read-write permissions on the destination account to the default identity of the source account. To confirm, click the \"Yes\" button."
},
"pointInTimeRestore": {
"title": "Point In Time Restore enabled",
"description": "To facilitate online container copy jobs, please update your \"{{accessName}}\" backup policy from periodic to continuous backup. Enabling continuous backup is required for this functionality.",
"tooltipContent": "Learn more about",
"tooltipHrefText": "Continuous Backup",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "Enable Point In Time Restore"
},
"onlineCopyEnabled": {
"title": "Online copy enabled",
"description": "Enable online container copy by clicking the button below on your \"{{accountName}}\" account.",
"hrefText": "Learn more about online copy jobs",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "Enable Online Copy",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Validating All versions and deletes change feed mode (preview)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Enabling All versions and deletes change feed mode (preview)...",
"enablingOnlineCopySpinnerLabel": "Enabling online copy on your \"{{accountName}}\" account ..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "Date & time",
"name": "Job name",
"status": "Status",
"completionPercentage": "Completion %",
"duration": "Duration",
"error": "Error message",
"mode": "Mode",
"actions": "Actions"
},
"actions": {
"pause": "Pause",
"resume": "Resume",
"complete": "Complete",
"viewDetails": "View Details"
},
"status": {
"pending": "Queued",
"inProgress": "Running",
"running": "Running",
"partitioning": "Running",
"paused": "Paused",
"completed": "Completed",
"failed": "Failed",
"faulted": "Failed",
"skipped": "Cancelled",
"cancelled": "Cancelled"
},
"dialog": {
"confirmButtonText": "Confirm",
"cancelButtonText": "Cancel"
}
}
} }
} }
+13 -10
View File
@@ -1,33 +1,36 @@
import { AccountOverride } from "Contracts/DataModels";
import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { isFabricNative } from "Platform/Fabric/FabricUtil";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export const isCapabilityEnabled = (capabilityName: string): boolean => { export const isCapabilityEnabled = (capabilityName: string, targetAccountOverride?: AccountOverride): boolean => {
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
if (databaseAccount && databaseAccount.properties && databaseAccount.properties.capabilities) { const capabilities = targetAccountOverride?.capabilities || databaseAccount?.properties?.capabilities;
return databaseAccount.properties.capabilities.some((capability) => capability.name === capabilityName); if (capabilities) {
return capabilities.some((capability) => capability.name === capabilityName);
} }
return false; return false;
}; };
export const isServerlessAccount = (): boolean => { export const isServerlessAccount = (targetAccountOverride?: AccountOverride): boolean => {
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const capacityMode = targetAccountOverride?.capacityMode || databaseAccount?.properties?.capacityMode;
return ( return (
databaseAccount?.properties?.capacityMode === Constants.CapacityMode.Serverless || capacityMode === Constants.CapacityMode.Serverless ||
isCapabilityEnabled(Constants.CapabilityNames.EnableServerless) isCapabilityEnabled(Constants.CapabilityNames.EnableServerless, targetAccountOverride)
); );
}; };
export const isVectorSearchEnabled = (): boolean => { export const isVectorSearchEnabled = (targetAccountOverride?: AccountOverride): boolean => {
return ( return (
userContext.apiType === "SQL" && userContext.apiType === "SQL" &&
(isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch) || isFabricNative()) (isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch, targetAccountOverride) || isFabricNative())
); );
}; };
export const isFullTextSearchPreviewFeaturesEnabled = (): boolean => { export const isFullTextSearchPreviewFeaturesEnabled = (targetAccountOverride?: AccountOverride): boolean => {
return ( return (
userContext.apiType === "SQL" && userContext.apiType === "SQL" &&
isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearchPreviewFeatures) isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearchPreviewFeatures, targetAccountOverride)
); );
}; };
+1 -1
View File
@@ -93,7 +93,7 @@ export const assignRole = async (
return null; return null;
} }
const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`; const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002`; // Built-in Contributor role definition ID for Cosmos DB
const roleAssignmentName = crypto.randomUUID(); const roleAssignmentName = crypto.randomUUID();
const path = `${accountScope}/sqlRoleAssignments/${roleAssignmentName}`; const path = `${accountScope}/sqlRoleAssignments/${roleAssignmentName}`;
+3
View File
@@ -141,6 +141,9 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes
params.set("nosqlRbacToken", nosqlRbacToken); params.set("nosqlRbacToken", nosqlRbacToken);
params.set("enableaaddataplane", "true"); params.set("enableaaddataplane", "true");
} }
if (enablecontainercopy) {
params.set("enablecontainercopy", "true");
}
break; break;
case TestAccount.SQLContainerCopyOnly: case TestAccount.SQLContainerCopyOnly:
@@ -18,7 +18,7 @@ test.describe("Container Copy - Offline Migration", () => {
let panel: Locator; let panel: Locator;
let frame: Frame; let frame: Frame;
let expectedJobName: string; let expectedJobName: string;
let targetAccountName: string; let sourceAccountName: string;
let expectedSubscriptionName: string; let expectedSubscriptionName: string;
let expectedCopyJobNameInitial: string; let expectedCopyJobNameInitial: string;
@@ -28,7 +28,7 @@ test.describe("Container Copy - Offline Migration", () => {
page = await browser.newPage(); page = await browser.newPage();
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly)); ({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
expectedJobName = `offline_test_job_${Date.now()}`; expectedJobName = `offline_test_job_${Date.now()}`;
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly); sourceAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
}); });
test.afterEach("Cleanup after offline migration test", async () => { test.afterEach("Cleanup after offline migration test", async () => {
@@ -53,7 +53,7 @@ test.describe("Container Copy - Offline Migration", () => {
// Setup subscription and account // Setup subscription and account
const subscriptionDropdown = panel.getByTestId("subscription-dropdown"); const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
const expectedAccountName = targetAccountName; const expectedAccountName = sourceAccountName;
expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText(); expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText();
await subscriptionDropdown.click(); await subscriptionDropdown.click();
@@ -185,8 +185,8 @@ test.describe("Container Copy - Offline Migration", () => {
// Verify job preview details // Verify job preview details
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob"); const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
await expect(previewContainer).toBeVisible(); await expect(previewContainer).toBeVisible();
await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(expectedSubscriptionName); await expect(previewContainer.getByTestId("destination-subscription-name")).toHaveText(expectedSubscriptionName);
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedAccountName); await expect(previewContainer.getByTestId("destination-account-name")).toHaveText(expectedAccountName);
const jobNameInput = previewContainer.getByTestId("job-name-textfield"); const jobNameInput = previewContainer.getByTestId("job-name-textfield");
await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial)); await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial));
@@ -15,14 +15,14 @@ test.describe("Container Copy - Online Migration", () => {
let wrapper: Locator; let wrapper: Locator;
let panel: Locator; let panel: Locator;
let frame: Frame; let frame: Frame;
let targetAccountName: string; let sourceAccountName: string;
test.beforeEach("Setup for online migration test", async ({ browser }) => { test.beforeEach("Setup for online migration test", async ({ browser }) => {
contexts = await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 }); contexts = await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
page = await browser.newPage(); page = await browser.newPage();
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly)); ({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly); sourceAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
}); });
test.afterEach("Cleanup after online migration test", async () => { test.afterEach("Cleanup after online migration test", async () => {
@@ -103,7 +103,7 @@ test.describe("Container Copy - Online Migration", () => {
// Verify job preview and create the online migration job // Verify job preview and create the online migration job
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob"); const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(targetAccountName); await expect(previewContainer.getByTestId("destination-account-name")).toHaveText(sourceAccountName);
const jobNameInput = previewContainer.getByTestId("job-name-textfield"); const jobNameInput = previewContainer.getByTestId("job-name-textfield");
const onlineMigrationJobName = await jobNameInput.inputValue(); const onlineMigrationJobName = await jobNameInput.inputValue();
@@ -112,7 +112,7 @@ test.describe("Container Copy - Online Migration", () => {
const copyJobCreationPromise = waitForApiResponse( const copyJobCreationPromise = waitForApiResponse(
page, page,
`${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}`, `${sourceAccountName}/dataTransferJobs/${onlineMigrationJobName}`,
"PUT", "PUT",
); );
await copyButton.click(); await copyButton.click();
@@ -149,7 +149,7 @@ test.describe("Container Copy - Online Migration", () => {
const pauseResponse = await waitForApiResponse( const pauseResponse = await waitForApiResponse(
page, page,
`${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}/pause`, `${sourceAccountName}/dataTransferJobs/${onlineMigrationJobName}/pause`,
"POST", "POST",
); );
expect(pauseResponse.ok()).toBe(true); expect(pauseResponse.ok()).toBe(true);
@@ -10,13 +10,14 @@ test.describe("Container Copy - Permission Screen Verification", () => {
let wrapper: Locator; let wrapper: Locator;
let panel: Locator; let panel: Locator;
let frame: Frame; let frame: Frame;
let sourceAccountName: string;
let targetAccountName: string; let targetAccountName: string;
let expectedSourceAccountName: string;
test.beforeEach("Setup for each test", async ({ browser }) => { test.beforeEach("Setup for each test", async ({ browser }) => {
page = await browser.newPage(); page = await browser.newPage();
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly)); ({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQL));
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly); targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
sourceAccountName = getAccountName(TestAccount.SQL);
}); });
test.afterEach("Cleanup after each test", async () => { test.afterEach("Cleanup after each test", async () => {
@@ -80,22 +81,14 @@ test.describe("Container Copy - Permission Screen Verification", () => {
const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all(); const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all();
const filteredItems = [];
for (const item of allDropdownItems) { for (const item of allDropdownItems) {
const testContent = (await item.textContent()) ?? ""; const testContent = (await item.textContent()) ?? "";
if (testContent.trim() !== targetAccountName.trim()) { if (testContent.trim() === targetAccountName.trim()) {
filteredItems.push(item); await item.click();
break;
} }
} }
if (filteredItems.length > 0) {
const firstDropdownItem = filteredItems[0];
expectedSourceAccountName = (await firstDropdownItem.textContent()) ?? "";
await firstDropdownItem.click();
} else {
throw new Error("No dropdown items available after filtering");
}
// Enable online migration mode // Enable online migration mode
const migrationTypeContainer = panel.getByTestId("migration-type"); const migrationTypeContainer = panel.getByTestId("migration-type");
const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i }); const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i });
@@ -111,7 +104,7 @@ test.describe("Container Copy - Permission Screen Verification", () => {
await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible(); await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible();
// Setup API mocking for the source account // Setup API mocking for the source account
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}**`, async (route) => { await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}**`, async (route) => {
const mockData = { const mockData = {
identity: { identity: {
type: "SystemAssigned", type: "SystemAssigned",
@@ -169,7 +162,7 @@ test.describe("Container Copy - Permission Screen Verification", () => {
// Verify new page opens with correct URL pattern // Verify new page opens with correct URL pattern
page.context().on("page", async (newPage) => { page.context().on("page", async (newPage) => {
const expectedUrlEndPattern = new RegExp( const expectedUrlEndPattern = new RegExp(
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${expectedSourceAccountName}/backupRestore`, `/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${sourceAccountName}/backupRestore`,
); );
expect(newPage.url()).toMatch(expectedUrlEndPattern); expect(newPage.url()).toMatch(expectedUrlEndPattern);
await newPage.close(); await newPage.close();
@@ -188,8 +181,10 @@ test.describe("Container Copy - Permission Screen Verification", () => {
await expect(pitrBtn).not.toBeVisible(); await expect(pitrBtn).not.toBeVisible();
// Setup additional API mocks for role assignments and permissions // Setup additional API mocks for role assignments and permissions
// In the redesigned flow, role assignments are checked on the SOURCE account (current account = sourceAccountName).
// The destination account (selectedAccountName) manages identity; source account holds the role assignments.
await page.route( await page.route(
`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/sqlRoleAssignments*`, `**/Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}/sqlRoleAssignments*`,
async (route) => { async (route) => {
await route.fulfill({ await route.fulfill({
status: 200, status: 200,
@@ -198,7 +193,7 @@ test.describe("Container Copy - Permission Screen Verification", () => {
value: [ value: [
{ {
principalId: "00-11-22-33", principalId: "00-11-22-33",
roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/77-88-99`, roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}/77-88-99`,
}, },
], ],
}), }),
@@ -213,14 +208,15 @@ test.describe("Container Copy - Permission Screen Verification", () => {
body: JSON.stringify({ body: JSON.stringify({
value: [ value: [
{ {
name: "00000000-0000-0000-0000-000000000001", // Built-in Cosmos DB Data Contributor role (read-write), required by checkTargetHasReadWriteRoleOnSource
name: "00000000-0000-0000-0000-000000000002",
}, },
], ],
}), }),
}); });
}); });
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}**`, async (route) => { await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}**`, async (route) => {
const mockData = { const mockData = {
identity: { identity: {
type: "SystemAssigned", type: "SystemAssigned",