mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-14 01:07:25 +01:00
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:
@@ -34,6 +34,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
databaseId: params.databaseId,
|
||||
databaseLevelThroughput: params.databaseLevelThroughput,
|
||||
offerThroughput: params.offerThroughput,
|
||||
targetAccountOverride: params.targetAccountOverride,
|
||||
};
|
||||
await createDatabase(createDatabaseParams);
|
||||
}
|
||||
@@ -63,7 +64,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
};
|
||||
|
||||
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);
|
||||
if (!isValid) {
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
@@ -122,9 +123,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
||||
};
|
||||
|
||||
const createResponse = await createUpdateSqlContainer(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId,
|
||||
params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup,
|
||||
params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
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" }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
|
||||
}
|
||||
|
||||
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();
|
||||
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,
|
||||
},
|
||||
};
|
||||
const createResponse = await createUpdateSqlDatabase(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
rpPayload,
|
||||
);
|
||||
const sub = params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
|
||||
const rg = params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
|
||||
const acct = params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name;
|
||||
const createResponse = await createUpdateSqlDatabase(sub, rg, acct, params.databaseId, rpPayload);
|
||||
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../CosmosClient");
|
||||
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { client } from "../CosmosClient";
|
||||
import { readDatabases } from "./readDatabases";
|
||||
import { readDatabases, readDatabasesWithARM } from "./readDatabases";
|
||||
|
||||
describe("readDatabases", () => {
|
||||
beforeAll(() => {
|
||||
@@ -42,3 +43,149 @@ describe("readDatabases", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
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 { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
@@ -96,10 +96,17 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
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;
|
||||
const { subscriptionId, resourceGroup, apiType, databaseAccount } = userContext;
|
||||
const accountName = databaseAccount.name;
|
||||
const subscriptionId = accountOverride?.subscriptionId ?? userContext.subscriptionId ?? "";
|
||||
const resourceGroup = accountOverride?.resourceGroup ?? userContext.resourceGroup ?? "";
|
||||
const accountName = accountOverride?.accountName ?? userContext?.databaseAccount?.name ?? "";
|
||||
const apiType = accountOverride?.apiType ?? userContext.apiType;
|
||||
|
||||
switch (apiType) {
|
||||
case "SQL":
|
||||
@@ -118,5 +125,5 @@ async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
|
||||
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) ?? [];
|
||||
}
|
||||
|
||||
@@ -406,11 +406,22 @@ export interface AutoPilotOfferSettings {
|
||||
targetMaxThroughput?: number;
|
||||
}
|
||||
|
||||
export interface AccountOverride {
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
accountName: string;
|
||||
capabilities: Capability[];
|
||||
capacityMode?: CapacityMode;
|
||||
enableFreeTier?: boolean;
|
||||
enableAnalyticalStorage?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateDatabaseParams {
|
||||
autoPilotMaxThroughput?: number;
|
||||
databaseId: string;
|
||||
databaseLevelThroughput?: boolean;
|
||||
offerThroughput?: number;
|
||||
targetAccountOverride?: AccountOverride;
|
||||
}
|
||||
|
||||
export interface CreateCollectionParamsBase {
|
||||
@@ -430,6 +441,7 @@ export interface CreateCollectionParamsBase {
|
||||
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
||||
createNewDatabase: boolean;
|
||||
collectionId: string;
|
||||
targetAccountOverride?: AccountOverride;
|
||||
}
|
||||
|
||||
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
||||
|
||||
@@ -457,13 +457,13 @@ describe("CopyJobActions", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscription: {} as any,
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "sub-123",
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -498,7 +498,7 @@ describe("CopyJobActions", () => {
|
||||
);
|
||||
|
||||
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(mockOnSuccess).toHaveBeenCalled();
|
||||
@@ -509,13 +509,13 @@ describe("CopyJobActions", () => {
|
||||
jobName: "cross-account-job",
|
||||
migrationType: "offline" as any,
|
||||
source: {
|
||||
subscription: {} as any,
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "sub-456",
|
||||
subscription: {} as any,
|
||||
account: { id: "account-2", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -528,7 +528,7 @@ describe("CopyJobActions", () => {
|
||||
await submitCreateCopyJob(mockState, mockOnSuccess);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -537,13 +537,13 @@ describe("CopyJobActions", () => {
|
||||
jobName: "failing-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscription: {} as any,
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "sub-123",
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -566,13 +566,13 @@ describe("CopyJobActions", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscription: {} as any,
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "sub-123",
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
CreateJobRequest,
|
||||
DataTransferJobGetResults,
|
||||
} from "../../../Utils/arm/generatedClients/dataTransferService/types";
|
||||
import ContainerCopyMessages from "../ContainerCopyMessages";
|
||||
import {
|
||||
convertTime,
|
||||
convertToCamelCase,
|
||||
@@ -35,7 +35,7 @@ export const openCreateCopyJobPanel = (explorer: Explorer) => {
|
||||
const sidePanelState = useSidePanel.getState();
|
||||
sidePanelState.setPanelHasConsole(false);
|
||||
sidePanelState.openSidePanel(
|
||||
ContainerCopyMessages.createCopyJobPanelTitle,
|
||||
t(Keys.containerCopy.createCopyJob.panelTitle),
|
||||
<CreateCopyJobScreensProvider explorer={explorer} />,
|
||||
"650px",
|
||||
);
|
||||
@@ -45,7 +45,7 @@ export const openCopyJobDetailsPanel = (job: CopyJobType) => {
|
||||
const sidePanelState = useSidePanel.getState();
|
||||
sidePanelState.setPanelHasConsole(false);
|
||||
sidePanelState.openSidePanel(
|
||||
ContainerCopyMessages.copyJobDetailsPanelTitle(job.Name),
|
||||
job.Name || t(Keys.containerCopy.jobDetails.panelTitleDefault),
|
||||
<CopyJobDetails job={job} />,
|
||||
"650px",
|
||||
);
|
||||
@@ -137,12 +137,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
|
||||
properties: {
|
||||
source: {
|
||||
component: "CosmosDBSql",
|
||||
...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }),
|
||||
databaseName: source?.databaseId,
|
||||
containerName: source?.containerId,
|
||||
},
|
||||
destination: {
|
||||
component: "CosmosDBSql",
|
||||
...(isSameAccount ? {} : { remoteAccountName: target?.account?.name }),
|
||||
databaseName: target?.databaseId,
|
||||
containerName: target?.containerId,
|
||||
},
|
||||
@@ -193,7 +193,7 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
|
||||
const pattern = new RegExp(`'(${statusList.join("|")})'`, "g");
|
||||
const normalizedErrorMessage = errorMessage.replace(
|
||||
pattern,
|
||||
`'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`,
|
||||
`'${t(Keys.containerCopy.monitorJobs.status.inProgress)}'`,
|
||||
);
|
||||
logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus");
|
||||
throw error;
|
||||
|
||||
@@ -5,10 +5,10 @@ import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
||||
import SunIcon from "../../../../images/SunIcon.svg";
|
||||
import { configContext, Platform } from "../../../ConfigContext";
|
||||
import { useThemeStore } from "../../../hooks/useTheme";
|
||||
import { Keys, t } from "../../../Localization";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as Actions from "../Actions/CopyJobActions";
|
||||
import ContainerCopyMessages from "../ContainerCopyMessages";
|
||||
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
|
||||
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
||||
|
||||
@@ -19,15 +19,15 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
|
||||
{
|
||||
key: "createCopyJob",
|
||||
iconSrc: AddIcon,
|
||||
label: ContainerCopyMessages.createCopyJobButtonLabel,
|
||||
ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel,
|
||||
label: t(Keys.containerCopy.commandBar.createCopyJobButtonLabel),
|
||||
ariaLabel: t(Keys.containerCopy.commandBar.createCopyJobButtonAriaLabel),
|
||||
onClick: () => Actions.openCreateCopyJobPanel(explorer),
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
iconSrc: RefreshIcon,
|
||||
label: ContainerCopyMessages.refreshButtonLabel,
|
||||
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
|
||||
label: t(Keys.common.refresh),
|
||||
ariaLabel: t(Keys.containerCopy.commandBar.refreshButtonAriaLabel),
|
||||
onClick: () => monitorCopyJobsRef?.refreshJobList(),
|
||||
},
|
||||
{
|
||||
@@ -48,8 +48,8 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
|
||||
buttons.push({
|
||||
key: "feedback",
|
||||
iconSrc: FeedbackIcon,
|
||||
label: ContainerCopyMessages.feedbackButtonLabel,
|
||||
ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel,
|
||||
label: t(Keys.containerCopy.commandBar.feedbackButtonLabel),
|
||||
ariaLabel: t(Keys.containerCopy.commandBar.feedbackButtonAriaLabel),
|
||||
onClick: () => {
|
||||
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 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",
|
||||
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: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null,
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
@@ -75,7 +69,13 @@ describe("CopyJobContext", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
target: {
|
||||
subscription: null,
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
});
|
||||
expect(contextValue.flow).toBeNull();
|
||||
expect(contextValue.contextError).toBeNull();
|
||||
@@ -598,8 +598,8 @@ describe("CopyJobContext", () => {
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined();
|
||||
expect(contextValue.copyJobState.source?.account?.name).toBeUndefined();
|
||||
expect(contextValue.copyJobState.source?.subscriptionId).toBe("test-subscription-id");
|
||||
expect(contextValue.copyJobState.source?.account?.name).toBe("test-account");
|
||||
});
|
||||
|
||||
it("should initialize target with userContext values", () => {
|
||||
@@ -616,11 +616,11 @@ describe("CopyJobContext", () => {
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id");
|
||||
expect(contextValue.copyJobState.target.account.name).toBe("test-account");
|
||||
expect(contextValue.copyJobState.target.subscription).toBeNull();
|
||||
expect(contextValue.copyJobState.target.account).toBeNull();
|
||||
});
|
||||
|
||||
it("should initialize sourceReadAccessFromTarget as false", () => {
|
||||
it("should initialize sourceReadWriteAccessFromTarget as false", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
@@ -634,7 +634,7 @@ describe("CopyJobContext", () => {
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false);
|
||||
expect(contextValue.copyJobState.sourceReadWriteAccessFromTarget).toBe(false);
|
||||
});
|
||||
|
||||
it("should initialize with empty database and container ids", () => {
|
||||
|
||||
@@ -23,18 +23,18 @@ const getInitialCopyJobState = (): CopyJobContextState => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null,
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: userContext.subscriptionId || "",
|
||||
account: userContext.databaseAccount || null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
target: {
|
||||
subscription: null,
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+12
-12
@@ -2,9 +2,9 @@ import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { DatabaseAccount } from "Contracts/DataModels";
|
||||
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { CopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import AddManagedIdentity from "./AddManagedIdentity";
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => {
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
};
|
||||
|
||||
const mockContextValue = {
|
||||
@@ -133,16 +133,16 @@ describe("AddManagedIdentity", () => {
|
||||
it("renders all required elements", () => {
|
||||
renderWithContext();
|
||||
|
||||
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.description)).toBeInTheDocument();
|
||||
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText)).toBeInTheDocument();
|
||||
expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.description))).toBeInTheDocument();
|
||||
expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.descriptionHrefText))).toBeInTheDocument();
|
||||
expect(screen.getByRole("switch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders description link with correct href", () => {
|
||||
renderWithContext();
|
||||
|
||||
const link = screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText);
|
||||
expect(link.closest("a")).toHaveAttribute("href", ContainerCopyMessages.addManagedIdentity.descriptionHref);
|
||||
const link = screen.getByText(t(Keys.containerCopy.addManagedIdentity.descriptionHrefText));
|
||||
expect(link.closest("a")).toHaveAttribute("href", t(Keys.containerCopy.addManagedIdentity.descriptionHref));
|
||||
expect(link.closest("a")).toHaveAttribute("target", "_blank");
|
||||
expect(link.closest("a")).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
@@ -175,7 +175,7 @@ describe("AddManagedIdentity", () => {
|
||||
const toggle = screen.getByRole("switch");
|
||||
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", () => {
|
||||
@@ -185,7 +185,7 @@ describe("AddManagedIdentity", () => {
|
||||
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", () => {
|
||||
const expectedDescription = ContainerCopyMessages.addManagedIdentity.enablementDescription(
|
||||
mockCopyJobState.target.account.name,
|
||||
);
|
||||
const expectedDescription = t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
|
||||
accountName: mockCopyJobState.source.account.name,
|
||||
});
|
||||
expect(screen.getByText(expectedDescription)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -220,7 +220,7 @@ describe("AddManagedIdentity", () => {
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
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");
|
||||
expect(toggle).not.toBeChecked();
|
||||
|
||||
+13
-11
@@ -1,7 +1,7 @@
|
||||
import { Link, Stack, Text, Toggle } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import InfoTooltip from "../Components/InfoTooltip";
|
||||
import PopoverMessage from "../Components/PopoverContainer";
|
||||
@@ -11,14 +11,14 @@ import useToggle from "./hooks/useToggle";
|
||||
|
||||
const managedIdentityTooltip = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.addManagedIdentity.tooltip.content}
|
||||
{t(Keys.containerCopy.addManagedIdentity.tooltipContent)}
|
||||
<Link
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
href={ContainerCopyMessages.addManagedIdentity.tooltip.href}
|
||||
href={t(Keys.containerCopy.addManagedIdentity.tooltipHref)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
|
||||
{t(Keys.containerCopy.addManagedIdentity.tooltipHrefText)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
@@ -32,9 +32,9 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
return (
|
||||
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
<Text className="themeText">
|
||||
{ContainerCopyMessages.addManagedIdentity.description} 
|
||||
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer">
|
||||
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText}
|
||||
{t(Keys.containerCopy.addManagedIdentity.description)} 
|
||||
<Link href={t(Keys.containerCopy.addManagedIdentity.descriptionHref)} target="_blank" rel="noopener noreferrer">
|
||||
{t(Keys.containerCopy.addManagedIdentity.descriptionHrefText)}
|
||||
</Link>{" "}
|
||||
|
||||
<InfoTooltip content={managedIdentityTooltip} />
|
||||
@@ -42,18 +42,20 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={systemAssigned}
|
||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||
onText={t(Keys.common.on)}
|
||||
offText={t(Keys.common.off)}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
<PopoverMessage
|
||||
isLoading={loading}
|
||||
visible={systemAssigned}
|
||||
title={ContainerCopyMessages.addManagedIdentity.enablementTitle}
|
||||
title={t(Keys.containerCopy.addManagedIdentity.enablementTitle)}
|
||||
onCancel={() => onToggle(null, false)}
|
||||
onPrimary={handleAddSystemIdentity}
|
||||
>
|
||||
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)}
|
||||
{t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
|
||||
accountName: copyJobState.source?.account?.name,
|
||||
})}
|
||||
</PopoverMessage>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
+56
-52
@@ -1,10 +1,10 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { Keys, t } from "Localization";
|
||||
import { CopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes";
|
||||
import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity";
|
||||
import AddReadWritePermissionToDefaultIdentity from "./AddReadWritePermissionToDefaultIdentity";
|
||||
|
||||
jest.mock("../../../../../Common/Logger", () => ({
|
||||
logError: jest.fn(),
|
||||
@@ -73,7 +73,7 @@ import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUti
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
import useToggle from "./hooks/useToggle";
|
||||
|
||||
describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
|
||||
const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>;
|
||||
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
|
||||
@@ -86,7 +86,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: { subscriptionId: "source-sub-id" } as Subscription,
|
||||
subscriptionId: "source-sub-id",
|
||||
account: {
|
||||
id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
|
||||
name: "source-account",
|
||||
@@ -96,12 +96,16 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
properties: {
|
||||
documentEndpoint: "https://source-account.documents.azure.com:443/",
|
||||
},
|
||||
identity: {
|
||||
principalId: "source-principal-id",
|
||||
type: "SystemAssigned",
|
||||
},
|
||||
},
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "target-sub-id",
|
||||
subscription: { subscriptionId: "target-sub-id" } as Subscription,
|
||||
account: {
|
||||
id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
|
||||
name: "target-account",
|
||||
@@ -119,7 +123,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: jest.fn(),
|
||||
setContextError: jest.fn(),
|
||||
@@ -133,7 +137,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
const renderComponent = (contextValue = mockContextValue) => {
|
||||
return render(
|
||||
<CopyJobContext.Provider value={contextValue}>
|
||||
<AddReadPermissionToDefaultIdentity />
|
||||
<AddReadWritePermissionToDefaultIdentity />
|
||||
</CopyJobContext.Provider>,
|
||||
);
|
||||
};
|
||||
@@ -164,12 +168,12 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render correctly when sourceReadAccessFromTarget is true", () => {
|
||||
it("should render correctly when sourceReadWriteAccessFromTarget is true", () => {
|
||||
const contextWithAccess = {
|
||||
...mockContextValue,
|
||||
copyJobState: {
|
||||
...mockContextValue.copyJobState,
|
||||
sourceReadAccessFromTarget: true,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
},
|
||||
};
|
||||
const { container } = renderComponent(contextWithAccess);
|
||||
@@ -180,7 +184,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
describe("Component Structure", () => {
|
||||
it("should display the description text", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument();
|
||||
expect(screen.getByText(t(Keys.containerCopy.readWritePermissionAssigned.description))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the info tooltip", () => {
|
||||
@@ -212,10 +216,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
expect(screen.getByTestId("popover-message")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("popover-title")).toHaveTextContent(
|
||||
ContainerCopyMessages.readPermissionAssigned.popoverTitle,
|
||||
t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle),
|
||||
);
|
||||
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);
|
||||
});
|
||||
|
||||
it("should call handleAddReadPermission when primary button is clicked", async () => {
|
||||
it("should call handleAddReadWritePermission when primary button is clicked", async () => {
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
accountName: "source-account",
|
||||
subscriptionId: "target-sub-id",
|
||||
resourceGroup: "target-rg",
|
||||
accountName: "target-account",
|
||||
});
|
||||
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
|
||||
|
||||
@@ -258,22 +262,22 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
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(() => {
|
||||
mockUseToggle.mockReturnValue([true, jest.fn()]);
|
||||
});
|
||||
|
||||
it("should successfully assign role and update context", async () => {
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
accountName: "source-account",
|
||||
subscriptionId: "target-sub-id",
|
||||
resourceGroup: "target-rg",
|
||||
accountName: "target-account",
|
||||
});
|
||||
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
|
||||
|
||||
@@ -284,10 +288,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAssignRole).toHaveBeenCalledWith(
|
||||
"source-sub-id",
|
||||
"source-rg",
|
||||
"source-account",
|
||||
"target-principal-id",
|
||||
"target-sub-id",
|
||||
"target-rg",
|
||||
"target-account",
|
||||
"source-principal-id",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -298,9 +302,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
it("should handle error when assignRole fails", async () => {
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
accountName: "source-account",
|
||||
subscriptionId: "target-sub-id",
|
||||
resourceGroup: "target-rg",
|
||||
accountName: "target-account",
|
||||
});
|
||||
mockAssignRole.mockRejectedValue(new Error("Permission denied"));
|
||||
|
||||
@@ -312,7 +316,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
await waitFor(() => {
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
"Permission denied",
|
||||
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
|
||||
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -323,9 +327,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
it("should handle error without message", async () => {
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
accountName: "source-account",
|
||||
subscriptionId: "target-sub-id",
|
||||
resourceGroup: "target-rg",
|
||||
accountName: "target-account",
|
||||
});
|
||||
mockAssignRole.mockRejectedValue({});
|
||||
|
||||
@@ -336,23 +340,23 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
"Error assigning read permission to default identity. Please try again later.",
|
||||
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
|
||||
"Error assigning read-write permission to default identity. Please try again later.",
|
||||
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
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 () => {
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
accountName: "source-account",
|
||||
subscriptionId: "target-sub-id",
|
||||
resourceGroup: "target-rg",
|
||||
accountName: "target-account",
|
||||
});
|
||||
|
||||
mockAssignRole.mockImplementation(
|
||||
@@ -371,9 +375,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
it.skip("should not assign role when assignRole returns falsy", async () => {
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
accountName: "source-account",
|
||||
subscriptionId: "target-sub-id",
|
||||
resourceGroup: "target-rg",
|
||||
accountName: "target-account",
|
||||
});
|
||||
mockAssignRole.mockResolvedValue(null);
|
||||
|
||||
@@ -431,10 +435,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
...mockContextValue,
|
||||
copyJobState: {
|
||||
...mockContextValue.copyJobState,
|
||||
target: {
|
||||
...mockContextValue.copyJobState.target,
|
||||
source: {
|
||||
...mockContextValue.copyJobState.source,
|
||||
account: {
|
||||
...mockContextValue.copyJobState.target.account!,
|
||||
...mockContextValue.copyJobState.source.account!,
|
||||
identity: {
|
||||
principalId: "",
|
||||
type: "SystemAssigned",
|
||||
@@ -446,9 +450,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
mockUseToggle.mockReturnValue([true, jest.fn()]);
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
accountName: "source-account",
|
||||
subscriptionId: "target-sub-id",
|
||||
resourceGroup: "target-rg",
|
||||
accountName: "target-account",
|
||||
});
|
||||
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
|
||||
|
||||
@@ -458,7 +462,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
fireEvent.click(primaryButton);
|
||||
|
||||
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()]);
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
accountName: "source-account",
|
||||
subscriptionId: "target-sub-id",
|
||||
resourceGroup: "target-rg",
|
||||
accountName: "target-account",
|
||||
});
|
||||
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
|
||||
|
||||
@@ -496,7 +500,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
expect(updatedState).toEqual({
|
||||
...mockContextValue.copyJobState,
|
||||
sourceReadAccessFromTarget: true,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+32
-29
@@ -1,8 +1,8 @@
|
||||
import { Link, Stack, Text, Toggle } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { logError } from "../../../../../Common/Logger";
|
||||
import { assignRole } from "../../../../../Utils/arm/RbacUtils";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
import InfoTooltip from "../Components/InfoTooltip";
|
||||
@@ -12,51 +12,54 @@ import useToggle from "./hooks/useToggle";
|
||||
|
||||
const TooltipContent = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
||||
{t(Keys.containerCopy.readWritePermissionAssigned.tooltipContent)}
|
||||
<Link
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
|
||||
href={t(Keys.containerCopy.readWritePermissionAssigned.tooltipHref)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
||||
{t(Keys.containerCopy.readWritePermissionAssigned.tooltipHrefText)}
|
||||
</Link>
|
||||
</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 { 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 selectedSourceAccount = source?.account;
|
||||
const selectedTargetAccount = target?.account;
|
||||
|
||||
try {
|
||||
const {
|
||||
subscriptionId: sourceSubscriptionId,
|
||||
resourceGroup: sourceResourceGroup,
|
||||
accountName: sourceAccountName,
|
||||
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
|
||||
subscriptionId: targetSubscriptionId,
|
||||
resourceGroup: targetResourceGroup,
|
||||
accountName: targetAccountName,
|
||||
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
|
||||
|
||||
setLoading(true);
|
||||
const assignedRole = await assignRole(
|
||||
sourceSubscriptionId,
|
||||
sourceResourceGroup,
|
||||
sourceAccountName,
|
||||
target?.account?.identity?.principalId ?? "",
|
||||
targetSubscriptionId,
|
||||
targetResourceGroup,
|
||||
targetAccountName,
|
||||
source?.account?.identity?.principalId ?? "",
|
||||
);
|
||||
|
||||
if (assignedRole) {
|
||||
setCopyJobState((prevState) => ({
|
||||
...prevState,
|
||||
sourceReadAccessFromTarget: true,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.message || "Error assigning read permission to default identity. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission");
|
||||
error.message || "Error assigning read-write permission to default identity. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission");
|
||||
setContextError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -66,14 +69,14 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
||||
return (
|
||||
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
<Text className="toggle-label">
|
||||
{ContainerCopyMessages.readPermissionAssigned.description} 
|
||||
{t(Keys.containerCopy.readWritePermissionAssigned.description)} 
|
||||
<InfoTooltip content={TooltipContent} />
|
||||
</Text>
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={readPermissionAssigned}
|
||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||
checked={readWritePermissionAssigned}
|
||||
onText={t(Keys.common.on)}
|
||||
offText={t(Keys.common.off)}
|
||||
onChange={onToggle}
|
||||
inlineLabel
|
||||
styles={{
|
||||
@@ -83,15 +86,15 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
||||
/>
|
||||
<PopoverMessage
|
||||
isLoading={loading}
|
||||
visible={readPermissionAssigned}
|
||||
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
|
||||
visible={readWritePermissionAssigned}
|
||||
title={t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle)}
|
||||
onCancel={() => onToggle(null, false)}
|
||||
onPrimary={handleAddReadPermission}
|
||||
onPrimary={handleAddReadWritePermission}
|
||||
>
|
||||
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
|
||||
{t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription)}
|
||||
</PopoverMessage>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddReadPermissionToDefaultIdentity;
|
||||
export default AddReadWritePermissionToDefaultIdentity;
|
||||
+17
-15
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, RenderResult } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { Keys, t } from "Localization";
|
||||
import { CopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||
import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes";
|
||||
@@ -43,12 +43,12 @@ jest.mock("./AddManagedIdentity", () => {
|
||||
return MockAddManagedIdentity;
|
||||
});
|
||||
|
||||
jest.mock("./AddReadPermissionToDefaultIdentity", () => {
|
||||
const MockAddReadPermissionToDefaultIdentity = () => {
|
||||
return <div data-testid="add-read-permission">Add Read Permission Component</div>;
|
||||
jest.mock("./AddReadWritePermissionToDefaultIdentity", () => {
|
||||
const MockAddReadWritePermissionToDefaultIdentity = () => {
|
||||
return <div data-testid="add-read-write-permission">Add Read-Write Permission Component</div>;
|
||||
};
|
||||
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
|
||||
return MockAddReadPermissionToDefaultIdentity;
|
||||
MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
|
||||
return MockAddReadWritePermissionToDefaultIdentity;
|
||||
});
|
||||
|
||||
jest.mock("./DefaultManagedIdentity", () => {
|
||||
@@ -85,18 +85,18 @@ describe("AssignPermissions Component", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: { subscriptionId: "source-sub" } as any,
|
||||
subscriptionId: "source-sub",
|
||||
account: { id: "source-account", name: "Source Account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "target-sub",
|
||||
subscription: { subscriptionId: "target-sub" } as any,
|
||||
account: { id: "target-account", name: "Target Account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ describe("AssignPermissions Component", () => {
|
||||
const copyJobState = createMockCopyJobState();
|
||||
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 () => {
|
||||
@@ -164,13 +164,13 @@ describe("AssignPermissions Component", () => {
|
||||
const copyJobState = createMockCopyJobState({
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
source: {
|
||||
subscription: { subscriptionId: "same-sub" } as any,
|
||||
subscriptionId: "same-sub",
|
||||
account: { id: "same-account", name: "Same Account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "same-sub",
|
||||
subscription: { subscriptionId: "same-sub" } as any,
|
||||
account: { id: "same-account", name: "Same Account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -179,7 +179,9 @@ describe("AssignPermissions Component", () => {
|
||||
|
||||
const { getByText } = renderWithContext(copyJobState);
|
||||
expect(
|
||||
getByText(ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription("Same Account")),
|
||||
getByText(
|
||||
t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, { accountName: "Same Account" }),
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -201,7 +203,7 @@ describe("AssignPermissions Component", () => {
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
id: "readPermissionAssigned",
|
||||
id: "readWritePermissionAssigned",
|
||||
title: "Read Permission Assigned",
|
||||
Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>,
|
||||
disabled: false,
|
||||
@@ -347,7 +349,7 @@ describe("AssignPermissions Component", () => {
|
||||
it("should handle missing account names", () => {
|
||||
const copyJobState = createMockCopyJobState({
|
||||
source: {
|
||||
subscription: { subscriptionId: "source-sub" } as any,
|
||||
subscriptionId: "source-sub",
|
||||
account: { id: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
import { Image, Stack, Text } from "@fluentui/react";
|
||||
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
|
||||
import { Keys, t } from "Localization";
|
||||
import React, { useEffect } from "react";
|
||||
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
|
||||
import WarningIcon from "../../../../../../images/warning.svg";
|
||||
import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { isIntraAccountCopy } from "../../../CopyJobUtils";
|
||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||
@@ -106,11 +106,11 @@ const AssignPermissions = () => {
|
||||
tokens={{ childrenGap: 20 }}
|
||||
>
|
||||
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
||||
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
|
||||
copyJobState?.source?.account?.name || "",
|
||||
)
|
||||
: ContainerCopyMessages.assignPermissions.crossAccountDescription}
|
||||
{isSameAccount && copyJobState?.migrationType === CopyJobMigrationType.Online
|
||||
? t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, {
|
||||
accountName: copyJobState?.source?.account?.name || "",
|
||||
})
|
||||
: t(Keys.containerCopy.assignPermissions.crossAccountDescription)}
|
||||
</Text>
|
||||
|
||||
{totalSectionsCount === 0 ? (
|
||||
|
||||
+20
-5
@@ -1,8 +1,8 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { CopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import DefaultManagedIdentity from "./DefaultManagedIdentity";
|
||||
|
||||
@@ -69,6 +69,12 @@ const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
|
||||
describe("DefaultManagedIdentity", () => {
|
||||
const mockCopyJobContextValue = {
|
||||
copyJobState: {
|
||||
source: {
|
||||
account: {
|
||||
name: "test-cosmos-account",
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos-account",
|
||||
},
|
||||
},
|
||||
target: {
|
||||
account: {
|
||||
name: "test-cosmos-account",
|
||||
@@ -166,7 +172,7 @@ describe("DefaultManagedIdentity", () => {
|
||||
expect(popover).toBeInTheDocument();
|
||||
|
||||
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");
|
||||
expect(content).toHaveTextContent(
|
||||
@@ -260,6 +266,12 @@ describe("DefaultManagedIdentity", () => {
|
||||
const contextValueWithoutAccount = {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
source: {
|
||||
account: {
|
||||
name: "",
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/",
|
||||
},
|
||||
},
|
||||
target: {
|
||||
account: {
|
||||
name: "",
|
||||
@@ -277,6 +289,9 @@ describe("DefaultManagedIdentity", () => {
|
||||
const contextValueWithNullAccount = {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
source: {
|
||||
account: null as DatabaseAccount | null,
|
||||
},
|
||||
target: {
|
||||
account: null as DatabaseAccount | null,
|
||||
},
|
||||
@@ -339,8 +354,8 @@ describe("DefaultManagedIdentity", () => {
|
||||
it("should display correct toggle button text", () => {
|
||||
renderComponent();
|
||||
|
||||
const onText = screen.queryByText(ContainerCopyMessages.toggleBtn.onText);
|
||||
const offText = screen.queryByText(ContainerCopyMessages.toggleBtn.offText);
|
||||
const onText = screen.queryByText(t(Keys.common.on));
|
||||
const offText = screen.queryByText(t(Keys.common.off));
|
||||
|
||||
expect(onText || offText).toBeTruthy();
|
||||
});
|
||||
@@ -348,7 +363,7 @@ describe("DefaultManagedIdentity", () => {
|
||||
it("should display correct link text in tooltip", () => {
|
||||
renderComponent();
|
||||
|
||||
const linkText = screen.getByText(ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText);
|
||||
const linkText = screen.getByText(t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText));
|
||||
expect(linkText).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+14
-9
@@ -1,7 +1,7 @@
|
||||
import { Link, Stack, Text, Toggle } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import InfoTooltip from "../Components/InfoTooltip";
|
||||
import PopoverMessage from "../Components/PopoverContainer";
|
||||
@@ -11,14 +11,14 @@ import useToggle from "./hooks/useToggle";
|
||||
|
||||
const managedIdentityTooltip = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content}
|
||||
{t(Keys.containerCopy.defaultManagedIdentity.tooltipContent)}
|
||||
<Link
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href}
|
||||
href={t(Keys.containerCopy.defaultManagedIdentity.tooltipHref)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
|
||||
{t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
@@ -32,14 +32,17 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
return (
|
||||
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
<div className="toggle-label">
|
||||
{ContainerCopyMessages.defaultManagedIdentity.description(copyJobState?.target?.account?.name)}
|
||||
{t(Keys.containerCopy.defaultManagedIdentity.description, {
|
||||
accountName: copyJobState?.source?.account?.name,
|
||||
})}{" "}
|
||||
|
||||
<InfoTooltip content={managedIdentityTooltip} />
|
||||
</div>
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={defaultSystemAssigned}
|
||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||
onText={t(Keys.common.on)}
|
||||
offText={t(Keys.common.off)}
|
||||
onChange={onToggle}
|
||||
inlineLabel
|
||||
styles={{
|
||||
@@ -50,11 +53,13 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
<PopoverMessage
|
||||
isLoading={loading}
|
||||
visible={defaultSystemAssigned}
|
||||
title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle}
|
||||
title={t(Keys.containerCopy.defaultManagedIdentity.popoverTitle)}
|
||||
onCancel={() => onToggle(null, false)}
|
||||
onPrimary={handleAddSystemIdentity}
|
||||
>
|
||||
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription(copyJobState?.target?.account?.name)}
|
||||
{t(Keys.containerCopy.defaultManagedIdentity.popoverDescription, {
|
||||
accountName: copyJobState?.source?.account?.name,
|
||||
})}
|
||||
</PopoverMessage>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
+25
-21
@@ -2,12 +2,12 @@ import "@testing-library/jest-dom";
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { DatabaseAccount } from "Contracts/DataModels";
|
||||
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
|
||||
import { CapabilityNames } from "../../../../../Common/Constants";
|
||||
import { logError } from "../../../../../Common/Logger";
|
||||
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { CopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import OnlineCopyEnabled from "./OnlineCopyEnabled";
|
||||
|
||||
@@ -97,7 +97,9 @@ describe("OnlineCopyEnabled", () => {
|
||||
it("should render the description with account name", () => {
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -105,10 +107,10 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const link = screen.getByRole("link", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.hrefText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.hrefText),
|
||||
});
|
||||
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("rel", "noopener noreferrer");
|
||||
});
|
||||
@@ -117,7 +119,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).not.toBeDisabled();
|
||||
@@ -134,7 +136,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const refreshButton = screen.queryByRole("button", {
|
||||
name: ContainerCopyMessages.refreshButtonLabel,
|
||||
name: t(Keys.common.refresh),
|
||||
});
|
||||
expect(refreshButton).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -167,7 +169,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -222,7 +224,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -246,7 +248,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -259,7 +261,9 @@ describe("OnlineCopyEnabled", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel("test-account")),
|
||||
screen.getByText(
|
||||
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { accountName: "test-account" }),
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -272,7 +276,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -306,7 +310,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -318,7 +322,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
});
|
||||
|
||||
const refreshButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.refreshButtonLabel,
|
||||
name: t(Keys.common.refresh),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -349,7 +353,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -379,7 +383,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -401,7 +405,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -418,7 +422,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -436,7 +440,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -450,7 +454,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
const refreshButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.refreshButtonLabel,
|
||||
name: t(Keys.common.refresh),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -536,7 +540,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent(contextWithNoCapabilities);
|
||||
|
||||
const enableButton = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
expect(enableButton).toBeInTheDocument();
|
||||
});
|
||||
@@ -547,7 +551,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.onlineCopyEnabled.buttonText,
|
||||
name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
+13
-9
@@ -1,12 +1,12 @@
|
||||
import { Link, PrimaryButton, Stack } from "@fluentui/react";
|
||||
import { DatabaseAccount } from "Contracts/DataModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
|
||||
import { CapabilityNames } from "../../../../../Common/Constants";
|
||||
import LoadingOverlay from "../../../../../Common/LoadingOverlay";
|
||||
import { logError } from "../../../../../Common/Logger";
|
||||
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
import { AccountValidatorFn } from "../../../Types/CopyJobTypes";
|
||||
@@ -76,21 +76,25 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
setShowRefreshButton(false);
|
||||
|
||||
try {
|
||||
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel);
|
||||
setLoaderMessage(t(Keys.containerCopy.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel));
|
||||
const sourAccountBeforeUpdate = await fetchDatabaseAccount(
|
||||
sourceSubscriptionId,
|
||||
sourceResourceGroup,
|
||||
sourceAccountName,
|
||||
);
|
||||
if (!sourAccountBeforeUpdate?.properties.enableAllVersionsAndDeletesChangeFeed) {
|
||||
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel);
|
||||
setLoaderMessage(t(Keys.containerCopy.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel));
|
||||
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
|
||||
properties: {
|
||||
enableAllVersionsAndDeletesChangeFeed: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel(sourceAccountName));
|
||||
setLoaderMessage(
|
||||
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, {
|
||||
accountName: sourceAccountName,
|
||||
}),
|
||||
);
|
||||
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
|
||||
properties: {
|
||||
capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }],
|
||||
@@ -132,16 +136,16 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
<LoadingOverlay isLoading={loading} label={loaderMessage} />
|
||||
<Stack.Item className="info-message">
|
||||
{ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")} 
|
||||
<Link href={ContainerCopyMessages.onlineCopyEnabled.href} target="_blank" rel="noopener noreferrer">
|
||||
{ContainerCopyMessages.onlineCopyEnabled.hrefText}
|
||||
{t(Keys.containerCopy.onlineCopyEnabled.description, { accountName: source?.account?.name || "" })} 
|
||||
<Link href={t(Keys.containerCopy.onlineCopyEnabled.href)} target="_blank" rel="noopener noreferrer">
|
||||
{t(Keys.containerCopy.onlineCopyEnabled.hrefText)}
|
||||
</Link>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{showRefreshButton ? (
|
||||
<PrimaryButton
|
||||
className="fullWidth"
|
||||
text={ContainerCopyMessages.refreshButtonLabel}
|
||||
text={t(Keys.common.refresh)}
|
||||
iconProps={{ iconName: "Refresh" }}
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
@@ -149,7 +153,7 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
) : (
|
||||
<PrimaryButton
|
||||
className="fullWidth"
|
||||
text={loading ? "" : ContainerCopyMessages.onlineCopyEnabled.buttonText}
|
||||
text={loading ? "" : t(Keys.containerCopy.onlineCopyEnabled.buttonText)}
|
||||
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||
disabled={loading}
|
||||
onClick={handleOnlineCopyEnable}
|
||||
|
||||
+3
-3
@@ -50,18 +50,18 @@ describe("PointInTimeRestore", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
|
||||
subscriptionId: "test-sub",
|
||||
account: mockSourceAccount,
|
||||
databaseId: "test-db",
|
||||
containerId: "test-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-sub",
|
||||
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
|
||||
account: mockSourceAccount,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
} as CopyJobContextState;
|
||||
|
||||
const mockSetCopyJobState = jest.fn();
|
||||
|
||||
+8
-8
@@ -1,10 +1,10 @@
|
||||
import { Link, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { DatabaseAccount } from "Contracts/DataModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
|
||||
import LoadingOverlay from "../../../../../Common/LoadingOverlay";
|
||||
import { logError } from "../../../../../Common/Logger";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
import { AccountValidatorFn } from "../../../Types/CopyJobTypes";
|
||||
@@ -12,14 +12,14 @@ import InfoTooltip from "../Components/InfoTooltip";
|
||||
|
||||
const tooltipContent = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.content}
|
||||
{t(Keys.containerCopy.pointInTimeRestore.tooltipContent)}
|
||||
<Link
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
href={ContainerCopyMessages.pointInTimeRestore.tooltip.href}
|
||||
href={t(Keys.containerCopy.pointInTimeRestore.tooltipHref)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
|
||||
{t(Keys.containerCopy.pointInTimeRestore.tooltipHrefText)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
@@ -119,9 +119,9 @@ const PointInTimeRestore: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{ContainerCopyMessages.pointInTimeRestore.description(source.account?.name ?? "")}
|
||||
{t(Keys.containerCopy.pointInTimeRestore.description, { accessName: source.account?.name ?? "" })}
|
||||
{tooltipContent && (
|
||||
<>
|
||||
{" "}
|
||||
@@ -134,7 +134,7 @@ const PointInTimeRestore: React.FC = () => {
|
||||
<PrimaryButton
|
||||
data-test="pointInTimeRestore:RefreshBtn"
|
||||
className="fullWidth"
|
||||
text={ContainerCopyMessages.refreshButtonLabel}
|
||||
text={t(Keys.common.refresh)}
|
||||
iconProps={{ iconName: "Refresh" }}
|
||||
onClick={handleRefresh}
|
||||
/>
|
||||
@@ -142,7 +142,7 @@ const PointInTimeRestore: React.FC = () => {
|
||||
<PrimaryButton
|
||||
data-test="pointInTimeRestore:PrimaryBtn"
|
||||
className="fullWidth"
|
||||
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
|
||||
text={loading ? "" : t(Keys.containerCopy.pointInTimeRestore.buttonText)}
|
||||
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
|
||||
disabled={loading}
|
||||
onClick={openWindowAndMonitor}
|
||||
|
||||
+5
-5
@@ -7,7 +7,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
|
||||
<span
|
||||
class="themeText css-110"
|
||||
>
|
||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.
|
||||
|
||||
<a
|
||||
class="ms-Link root-111"
|
||||
@@ -95,7 +95,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
||||
<span
|
||||
class="themeText css-110"
|
||||
>
|
||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.
|
||||
|
||||
<a
|
||||
class="ms-Link root-111"
|
||||
@@ -204,7 +204,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
|
||||
<span
|
||||
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>
|
||||
<div
|
||||
class="ms-Stack css-125"
|
||||
@@ -267,7 +267,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
||||
<span
|
||||
class="themeText css-110"
|
||||
>
|
||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.
|
||||
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.
|
||||
|
||||
<a
|
||||
class="ms-Link root-111"
|
||||
@@ -359,7 +359,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
|
||||
<span
|
||||
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>
|
||||
<div
|
||||
class="ms-Stack css-125"
|
||||
|
||||
+20
-20
@@ -1,6 +1,6 @@
|
||||
// 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
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -8,7 +8,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
||||
<span
|
||||
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
|
||||
data-testid="info-tooltip"
|
||||
@@ -24,7 +24,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read permissions.
|
||||
Read-write permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
||||
</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
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -71,7 +71,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
||||
<span
|
||||
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
|
||||
data-testid="info-tooltip"
|
||||
@@ -87,7 +87,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read permissions.
|
||||
Read-write permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -126,7 +126,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
|
||||
</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
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -134,7 +134,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
<span
|
||||
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
|
||||
data-testid="info-tooltip"
|
||||
@@ -150,7 +150,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read permissions.
|
||||
Read-write permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -189,7 +189,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
</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
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -197,7 +197,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
<span
|
||||
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
|
||||
data-testid="info-tooltip"
|
||||
@@ -213,7 +213,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read permissions.
|
||||
Read-write permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -255,12 +255,12 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
<div
|
||||
data-testid="popover-title"
|
||||
>
|
||||
Read permissions assigned to default identity.
|
||||
Assign read-write permissions to default identity.
|
||||
</div>
|
||||
<div
|
||||
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>
|
||||
<button
|
||||
data-testid="popover-cancel"
|
||||
@@ -277,7 +277,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
</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
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -285,7 +285,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
<span
|
||||
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
|
||||
data-testid="info-tooltip"
|
||||
@@ -301,7 +301,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read permissions.
|
||||
Read-write permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -340,7 +340,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
</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
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -348,7 +348,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
<span
|
||||
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
|
||||
data-testid="info-tooltip"
|
||||
@@ -364,7 +364,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read permissions.
|
||||
Read-write permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+6
-6
@@ -9,7 +9,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
||||
<span
|
||||
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>
|
||||
<div
|
||||
class="ms-Stack css-111"
|
||||
@@ -212,7 +212,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
<span
|
||||
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>
|
||||
<div
|
||||
class="ms-Stack css-111"
|
||||
@@ -618,7 +618,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
||||
<span
|
||||
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>
|
||||
<div
|
||||
class="ms-Stack css-111"
|
||||
@@ -1153,7 +1153,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
||||
<span
|
||||
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>
|
||||
<div
|
||||
class="ms-Stack css-111"
|
||||
@@ -1307,7 +1307,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
||||
<span
|
||||
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>
|
||||
<div
|
||||
data-testid="shimmer-tree"
|
||||
@@ -1329,7 +1329,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
||||
<span
|
||||
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>
|
||||
<div
|
||||
data-testid="shimmer-tree"
|
||||
|
||||
+11
-6
@@ -9,7 +9,8 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
|
||||
class="toggle-label"
|
||||
>
|
||||
Set the system-assigned managed identity as default for "" by switching it on.
|
||||
|
||||
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
>
|
||||
@@ -71,8 +72,9 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
|
||||
<div
|
||||
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
|
||||
data-testid="info-tooltip"
|
||||
>
|
||||
@@ -135,7 +137,8 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
|
||||
class="toggle-label"
|
||||
>
|
||||
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
|
||||
|
||||
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
>
|
||||
@@ -227,7 +230,8 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
|
||||
class="toggle-label"
|
||||
>
|
||||
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
|
||||
|
||||
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
>
|
||||
@@ -290,7 +294,8 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
|
||||
class="toggle-label"
|
||||
>
|
||||
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
|
||||
|
||||
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
>
|
||||
|
||||
+8
-8
@@ -26,18 +26,18 @@ const useManagedIdentity = (
|
||||
const handleAddSystemIdentity = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const selectedTargetAccount = copyJobState?.target?.account;
|
||||
const selectedSourceAccount = copyJobState?.source?.account;
|
||||
const {
|
||||
subscriptionId: targetSubscriptionId,
|
||||
resourceGroup: targetResourceGroup,
|
||||
accountName: targetAccountName,
|
||||
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id) || {};
|
||||
subscriptionId: sourceSubscriptionId,
|
||||
resourceGroup: sourceResourceGroup,
|
||||
accountName: sourceAccountName,
|
||||
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id) || {};
|
||||
|
||||
const updatedAccount = await updateIdentityFn(targetSubscriptionId, targetResourceGroup, targetAccountName);
|
||||
const updatedAccount = await updateIdentityFn(sourceSubscriptionId, sourceResourceGroup, sourceAccountName);
|
||||
if (updatedAccount) {
|
||||
setCopyJobState((prevState) => ({
|
||||
...prevState,
|
||||
target: { ...prevState.target, account: updatedAccount },
|
||||
source: { ...prevState.source, account: updatedAccount },
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -46,7 +46,7 @@ const useManagedIdentity = (
|
||||
setContextError(errorMessage);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [copyJobState?.target?.account?.id, updateIdentityFn, setCopyJobState]);
|
||||
}, [copyJobState?.source?.account?.id, updateIdentityFn, setCopyJobState]);
|
||||
|
||||
return { loading, handleAddSystemIdentity };
|
||||
};
|
||||
|
||||
+55
-40
@@ -13,7 +13,7 @@ import {
|
||||
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||
import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache";
|
||||
import usePermissionSections, {
|
||||
checkTargetHasReaderRoleOnSource,
|
||||
checkTargetHasReadWriteRoleOnSource,
|
||||
PermissionGroupConfig,
|
||||
SECTION_IDS,
|
||||
} from "./usePermissionsSection";
|
||||
@@ -40,12 +40,12 @@ jest.mock("../AddManagedIdentity", () => {
|
||||
return MockAddManagedIdentity;
|
||||
});
|
||||
|
||||
jest.mock("../AddReadPermissionToDefaultIdentity", () => {
|
||||
const MockAddReadPermissionToDefaultIdentity = () => {
|
||||
return <div data-testid="add-read-permission">AddReadPermissionToDefaultIdentity</div>;
|
||||
jest.mock("../AddReadWritePermissionToDefaultIdentity", () => {
|
||||
const MockAddReadWritePermissionToDefaultIdentity = () => {
|
||||
return <div data-testid="add-read-write-permission">AddReadWritePermissionToDefaultIdentity</div>;
|
||||
};
|
||||
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
|
||||
return MockAddReadPermissionToDefaultIdentity;
|
||||
MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
|
||||
return MockAddReadWritePermissionToDefaultIdentity;
|
||||
});
|
||||
|
||||
jest.mock("../DefaultManagedIdentity", () => {
|
||||
@@ -133,7 +133,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -152,7 +152,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -193,7 +193,7 @@ describe("usePermissionsSection", () => {
|
||||
expect(capturedResult[0].sections.map((s) => s.id)).toEqual([
|
||||
SECTION_IDS.addManagedIdentity,
|
||||
SECTION_IDS.defaultManagedIdentity,
|
||||
SECTION_IDS.readPermissionAssigned,
|
||||
SECTION_IDS.readWritePermissionAssigned,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -222,7 +222,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -284,16 +284,19 @@ describe("usePermissionsSection", () => {
|
||||
describe("Section validation", () => {
|
||||
it("should validate addManagedIdentity section correctly", async () => {
|
||||
const stateWithSystemAssigned = createMockState({
|
||||
target: {
|
||||
source: {
|
||||
account: {
|
||||
id: "target-account-id",
|
||||
name: "target-account",
|
||||
id: "source-account-id",
|
||||
name: "source-account",
|
||||
identity: {
|
||||
type: IdentityType.SystemAssigned,
|
||||
principalId: "principal-123",
|
||||
},
|
||||
properties: {
|
||||
defaultIdentity: DefaultIdentityType.FirstPartyIdentity,
|
||||
backupPolicy: {
|
||||
type: BackupPolicyType.Periodic,
|
||||
},
|
||||
capabilities: [],
|
||||
},
|
||||
location: "",
|
||||
type: "",
|
||||
@@ -322,16 +325,20 @@ describe("usePermissionsSection", () => {
|
||||
|
||||
it("should validate defaultManagedIdentity section correctly", async () => {
|
||||
const stateWithSystemAssignedIdentity = createMockState({
|
||||
target: {
|
||||
source: {
|
||||
account: {
|
||||
id: "target-account-id",
|
||||
name: "target-account",
|
||||
id: "source-account-id",
|
||||
name: "source-account",
|
||||
identity: {
|
||||
type: IdentityType.SystemAssigned,
|
||||
principalId: "principal-123",
|
||||
},
|
||||
properties: {
|
||||
defaultIdentity: DefaultIdentityType.SystemAssignedIdentity,
|
||||
backupPolicy: {
|
||||
type: BackupPolicyType.Periodic,
|
||||
},
|
||||
capabilities: [],
|
||||
},
|
||||
location: "",
|
||||
type: "",
|
||||
@@ -358,16 +365,17 @@ describe("usePermissionsSection", () => {
|
||||
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[] = [
|
||||
{
|
||||
id: "role-1",
|
||||
name: "Custom Role",
|
||||
name: "00000000-0000-0000-0000-000000000002",
|
||||
permissions: [
|
||||
{
|
||||
dataActions: [
|
||||
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
|
||||
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
|
||||
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -383,16 +391,20 @@ describe("usePermissionsSection", () => {
|
||||
mockedRbacUtils.fetchRoleDefinitions.mockResolvedValue(mockRoleDefinitions);
|
||||
|
||||
const state = createMockState({
|
||||
target: {
|
||||
source: {
|
||||
account: {
|
||||
id: "target-account-id",
|
||||
name: "target-account",
|
||||
id: "source-account-id",
|
||||
name: "source-account",
|
||||
identity: {
|
||||
type: IdentityType.SystemAssigned,
|
||||
principalId: "principal-123",
|
||||
},
|
||||
properties: {
|
||||
defaultIdentity: DefaultIdentityType.SystemAssignedIdentity,
|
||||
backupPolicy: {
|
||||
type: BackupPolicyType.Periodic,
|
||||
},
|
||||
capabilities: [],
|
||||
},
|
||||
location: "",
|
||||
type: "",
|
||||
@@ -407,7 +419,9 @@ describe("usePermissionsSection", () => {
|
||||
render(<TestWrapper state={state} onResult={noop} />);
|
||||
|
||||
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(
|
||||
@@ -435,7 +449,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -476,7 +490,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -546,7 +560,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -568,12 +582,12 @@ describe("usePermissionsSection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
it("should return true for built-in Reader role", () => {
|
||||
describe("checkTargetHasReadWriteRoleOnSource", () => {
|
||||
it("should return true for built-in Contributor role", () => {
|
||||
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
|
||||
{
|
||||
id: "role-1",
|
||||
name: "00000000-0000-0000-0000-000000000001",
|
||||
name: "00000000-0000-0000-0000-000000000002",
|
||||
permissions: [],
|
||||
assignableScopes: [],
|
||||
resourceGroup: "",
|
||||
@@ -583,20 +597,21 @@ describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
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[] = [
|
||||
{
|
||||
id: "role-1",
|
||||
name: "Custom Reader Role",
|
||||
name: "Custom Contributor Role",
|
||||
permissions: [
|
||||
{
|
||||
dataActions: [
|
||||
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
|
||||
"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);
|
||||
});
|
||||
|
||||
@@ -630,12 +645,12 @@ describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for empty role definitions", () => {
|
||||
const result = checkTargetHasReaderRoleOnSource([]);
|
||||
const result = checkTargetHasReadWriteRoleOnSource([]);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
@@ -653,11 +668,11 @@ describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
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[] = [
|
||||
{
|
||||
id: "role-1",
|
||||
@@ -675,7 +690,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
},
|
||||
{
|
||||
id: "role-2",
|
||||
name: "00000000-0000-0000-0000-000000000001",
|
||||
name: "00000000-0000-0000-0000-000000000002",
|
||||
permissions: [],
|
||||
assignableScopes: [],
|
||||
resourceGroup: "",
|
||||
@@ -685,7 +700,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+63
-43
@@ -1,7 +1,7 @@
|
||||
import { Keys, t } from "Localization";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { CapabilityNames } from "../../../../../../Common/Constants";
|
||||
import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { getAccountDetailsFromResourceId, getContainerIdentifiers, isIntraAccountCopy } from "../../../../CopyJobUtils";
|
||||
import {
|
||||
BackupPolicyType,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
|
||||
import AddManagedIdentity from "../AddManagedIdentity";
|
||||
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
|
||||
import AddReadWritePermissionToDefaultIdentity from "../AddReadWritePermissionToDefaultIdentity";
|
||||
import DefaultManagedIdentity from "../DefaultManagedIdentity";
|
||||
import OnlineCopyEnabled from "../OnlineCopyEnabled";
|
||||
import PointInTimeRestore from "../PointInTimeRestore";
|
||||
@@ -36,58 +36,60 @@ export interface PermissionGroupConfig {
|
||||
export const SECTION_IDS = {
|
||||
addManagedIdentity: "addManagedIdentity",
|
||||
defaultManagedIdentity: "defaultManagedIdentity",
|
||||
readPermissionAssigned: "readPermissionAssigned",
|
||||
readWritePermissionAssigned: "readWritePermissionAssigned",
|
||||
pointInTimeRestore: "pointInTimeRestore",
|
||||
onlineCopyEnabled: "onlineCopyEnabled",
|
||||
} as const;
|
||||
|
||||
const COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID = "00000000-0000-0000-0000-000000000002";
|
||||
|
||||
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
||||
{
|
||||
id: SECTION_IDS.addManagedIdentity,
|
||||
title: ContainerCopyMessages.addManagedIdentity.title,
|
||||
title: t(Keys.containerCopy.addManagedIdentity.title),
|
||||
Component: AddManagedIdentity,
|
||||
disabled: true,
|
||||
validate: (state: CopyJobContextState) => {
|
||||
const targetAccountIdentityType = (state?.target?.account?.identity?.type ?? "").toLowerCase();
|
||||
const sourceAccountIdentityType = (state?.source?.account?.identity?.type ?? "").toLowerCase();
|
||||
return (
|
||||
targetAccountIdentityType === IdentityType.SystemAssigned ||
|
||||
targetAccountIdentityType === IdentityType.UserAssigned
|
||||
sourceAccountIdentityType === IdentityType.SystemAssigned ||
|
||||
sourceAccountIdentityType === IdentityType.UserAssigned
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: SECTION_IDS.defaultManagedIdentity,
|
||||
title: ContainerCopyMessages.defaultManagedIdentity.title,
|
||||
title: t(Keys.containerCopy.defaultManagedIdentity.title),
|
||||
Component: DefaultManagedIdentity,
|
||||
disabled: true,
|
||||
validate: (state: CopyJobContextState) => {
|
||||
const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase();
|
||||
return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
|
||||
const sourceAccountDefaultIdentity = (state?.source?.account?.properties?.defaultIdentity ?? "").toLowerCase();
|
||||
return sourceAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: SECTION_IDS.readPermissionAssigned,
|
||||
title: ContainerCopyMessages.readPermissionAssigned.title,
|
||||
Component: AddReadPermissionToDefaultIdentity,
|
||||
id: SECTION_IDS.readWritePermissionAssigned,
|
||||
title: t(Keys.containerCopy.readWritePermissionAssigned.title),
|
||||
Component: AddReadWritePermissionToDefaultIdentity,
|
||||
disabled: true,
|
||||
validate: async (state: CopyJobContextState) => {
|
||||
const principalId = state?.target?.account?.identity?.principalId;
|
||||
const selectedSourceAccount = state?.source?.account;
|
||||
const principalId = state?.source?.account?.identity?.principalId;
|
||||
const selectedTargetAccount = state?.target?.account;
|
||||
const {
|
||||
subscriptionId: sourceSubscriptionId,
|
||||
resourceGroup: sourceResourceGroup,
|
||||
accountName: sourceAccountName,
|
||||
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id);
|
||||
subscriptionId: targetSubscriptionId,
|
||||
resourceGroup: targetResourceGroup,
|
||||
accountName: targetAccountName,
|
||||
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
|
||||
|
||||
const rolesAssigned = await fetchRoleAssignments(
|
||||
sourceSubscriptionId,
|
||||
sourceResourceGroup,
|
||||
sourceAccountName,
|
||||
targetSubscriptionId,
|
||||
targetResourceGroup,
|
||||
targetAccountName,
|
||||
principalId,
|
||||
);
|
||||
|
||||
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[] = [
|
||||
{
|
||||
id: SECTION_IDS.pointInTimeRestore,
|
||||
title: ContainerCopyMessages.pointInTimeRestore.title,
|
||||
title: t(Keys.containerCopy.pointInTimeRestore.title),
|
||||
Component: PointInTimeRestore,
|
||||
disabled: true,
|
||||
validate: (state: CopyJobContextState) => {
|
||||
@@ -105,7 +107,7 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
|
||||
},
|
||||
{
|
||||
id: SECTION_IDS.onlineCopyEnabled,
|
||||
title: ContainerCopyMessages.onlineCopyEnabled.title,
|
||||
title: t(Keys.containerCopy.onlineCopyEnabled.title),
|
||||
Component: OnlineCopyEnabled,
|
||||
disabled: true,
|
||||
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 {
|
||||
return roleDefinitions?.some(
|
||||
(role) =>
|
||||
role.name === "00000000-0000-0000-0000-000000000001" ||
|
||||
role.permissions.some(
|
||||
(permission) =>
|
||||
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata") &&
|
||||
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read"),
|
||||
),
|
||||
);
|
||||
export function checkTargetHasReadWriteRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
|
||||
return roleDefinitions?.some((role) => {
|
||||
if (role.name === COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dataActions = role.permissions?.flatMap((permission) => permission.dataActions ?? []) ?? [];
|
||||
|
||||
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) {
|
||||
groups.push({
|
||||
id: "crossAccountConfigs",
|
||||
title: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.title,
|
||||
description: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.description(
|
||||
sourceAccountName,
|
||||
targetAccountName,
|
||||
),
|
||||
title: t(Keys.containerCopy.assignPermissions.crossAccountConfiguration.title),
|
||||
description: t(Keys.containerCopy.assignPermissions.crossAccountConfiguration.description, {
|
||||
sourceAccount: sourceAccountName,
|
||||
destinationAccount: targetAccountName,
|
||||
}),
|
||||
sections: crossAccountSections,
|
||||
});
|
||||
}
|
||||
@@ -206,8 +224,10 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
|
||||
if (state.migrationType === CopyJobMigrationType.Online) {
|
||||
groups.push({
|
||||
id: "onlineConfigs",
|
||||
title: ContainerCopyMessages.assignPermissions.onlineConfiguration.title,
|
||||
description: ContainerCopyMessages.assignPermissions.onlineConfiguration.description(sourceAccountName),
|
||||
title: t(Keys.containerCopy.assignPermissions.onlineConfiguration.title),
|
||||
description: t(Keys.containerCopy.assignPermissions.onlineConfiguration.description, {
|
||||
accountName: sourceAccountName,
|
||||
}),
|
||||
sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS],
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { Keys, t } from "Localization";
|
||||
import PopoverMessage from "./PopoverContainer";
|
||||
|
||||
jest.mock("../../../../../Common/LoadingOverlay", () => {
|
||||
@@ -181,7 +181,7 @@ describe("PopoverMessage Component", () => {
|
||||
it("should use correct loading overlay label", () => {
|
||||
render(<PopoverMessage {...defaultProps} isLoading={true} />);
|
||||
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/display-name */
|
||||
import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import LoadingOverlay from "../../../../../Common/LoadingOverlay";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
|
||||
interface PopoverContainerProps {
|
||||
isLoading?: boolean;
|
||||
@@ -22,7 +22,7 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
|
||||
tokens={{ childrenGap: 20 }}
|
||||
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 }}>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
+10
-8
@@ -4,8 +4,8 @@ import { CopyJobMigrationType } from "Explorer/ContainerCopy/Enums/CopyJobEnums"
|
||||
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import AddCollectionPanelWrapper from "./AddCollectionPanelWrapper";
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: null,
|
||||
@@ -109,7 +109,9 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument();
|
||||
expect(container.querySelector(".addCollectionPanelHeader")).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();
|
||||
});
|
||||
|
||||
@@ -138,7 +140,7 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
it("should set header text to create container heading on mount", () => {
|
||||
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", () => {
|
||||
@@ -146,13 +148,13 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createCopyJobPanelTitle);
|
||||
expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.createCopyJob.panelTitle));
|
||||
});
|
||||
|
||||
it("should not change header text if already set correctly", () => {
|
||||
const modifiedSidePanelState = {
|
||||
...mockSidePanelState,
|
||||
headerText: ContainerCopyMessages.createContainerHeading,
|
||||
headerText: t(Keys.containerCopy.selectContainers.createContainerHeading),
|
||||
};
|
||||
|
||||
mockUseSidePanel.getState = jest.fn().mockReturnValue(modifiedSidePanelState);
|
||||
@@ -245,10 +247,10 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
describe("Component Lifecycle", () => {
|
||||
it("should properly cleanup on unmount", () => {
|
||||
const { unmount } = render(<AddCollectionPanelWrapper />);
|
||||
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createContainerHeading);
|
||||
expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.selectContainers.createContainerHeading));
|
||||
mockSetHeaderText.mockClear();
|
||||
unmount();
|
||||
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createCopyJobPanelTitle);
|
||||
expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.createCopyJob.panelTitle));
|
||||
});
|
||||
|
||||
it("should re-render correctly when props change", () => {
|
||||
|
||||
+110
-9
@@ -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 { useSidePanel } from "hooks/useSidePanel";
|
||||
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 ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
|
||||
type AddCollectionPanelWrapperProps = {
|
||||
explorer?: Explorer;
|
||||
@@ -13,18 +16,88 @@ type AddCollectionPanelWrapperProps = {
|
||||
};
|
||||
|
||||
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(() => {
|
||||
const sidePanelStore = useSidePanel.getState();
|
||||
if (sidePanelStore.headerText !== ContainerCopyMessages.createContainerHeading) {
|
||||
sidePanelStore.setHeaderText(ContainerCopyMessages.createContainerHeading);
|
||||
if (sidePanelStore.headerText !== t(Keys.containerCopy.selectContainers.createContainerHeading)) {
|
||||
sidePanelStore.setHeaderText(t(Keys.containerCopy.selectContainers.createContainerHeading));
|
||||
}
|
||||
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(
|
||||
(collectionData: { databaseId: string; collectionId: string }) => {
|
||||
setCopyJobState(
|
||||
@@ -38,13 +111,41 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
||||
[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 (
|
||||
<Stack className="addCollectionPanelWrapper">
|
||||
<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 className="addCollectionPanelBody">
|
||||
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
||||
<AddCollectionPanel
|
||||
explorer={explorer}
|
||||
isCopyJobFlow={true}
|
||||
onSubmitSuccess={handleAddCollectionSuccess}
|
||||
targetAccountOverride={targetAccountOverride}
|
||||
externalDatabaseOptions={destinationDatabases}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
+20
-20
@@ -3,19 +3,19 @@
|
||||
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
||||
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||
>
|
||||
<span
|
||||
class="themeText css-111"
|
||||
class="themeText css-117"
|
||||
>
|
||||
Select the properties for your container.
|
||||
Configure the properties for the new container.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelBody css-110"
|
||||
class="ms-StackItem addCollectionPanelBody css-116"
|
||||
>
|
||||
<div
|
||||
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`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
||||
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||
>
|
||||
<span
|
||||
class="themeText css-111"
|
||||
class="themeText css-117"
|
||||
>
|
||||
Select the properties for your container.
|
||||
Configure the properties for the new container.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelBody css-110"
|
||||
class="ms-StackItem addCollectionPanelBody css-116"
|
||||
>
|
||||
<div
|
||||
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`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
||||
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||
>
|
||||
<span
|
||||
class="themeText css-111"
|
||||
class="themeText css-117"
|
||||
>
|
||||
Select the properties for your container.
|
||||
Configure the properties for the new container.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelBody css-110"
|
||||
class="ms-StackItem addCollectionPanelBody css-116"
|
||||
>
|
||||
<div
|
||||
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`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
||||
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||
>
|
||||
<span
|
||||
class="themeText css-111"
|
||||
class="themeText css-117"
|
||||
>
|
||||
Select the properties for your container.
|
||||
Configure the properties for the new container.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelBody css-110"
|
||||
class="ms-StackItem addCollectionPanelBody css-116"
|
||||
>
|
||||
<div
|
||||
data-testid="add-collection-panel"
|
||||
|
||||
+15
-15
@@ -87,18 +87,18 @@ describe("PreviewCopyJob", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "target-database",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -146,7 +146,7 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with missing source subscription information", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
@@ -165,7 +165,7 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with missing source account information", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: null,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
@@ -184,13 +184,13 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with undefined database and container names", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
@@ -219,7 +219,7 @@ describe("PreviewCopyJob", () => {
|
||||
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: longNameSubscription,
|
||||
subscriptionId: longNameSubscription.subscriptionId,
|
||||
account: longNameAccount,
|
||||
databaseId: "long-database-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", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "test-db_with@special#chars",
|
||||
containerId: "test-container_with@special#chars",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "target-db_with@special#chars",
|
||||
containerId: "target-container_with@special#chars",
|
||||
@@ -285,12 +285,12 @@ describe("PreviewCopyJob", () => {
|
||||
|
||||
const mockContext = createMockContext({
|
||||
target: {
|
||||
subscriptionId: "target-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: targetAccount,
|
||||
databaseId: "target-database",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: true,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
@@ -350,7 +350,7 @@ describe("PreviewCopyJob", () => {
|
||||
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it("should display proper field labels from ContainerCopyMessages", () => {
|
||||
it("should display proper field labels", () => {
|
||||
const mockContext = createMockContext();
|
||||
|
||||
const { getByText } = render(
|
||||
@@ -360,7 +360,7 @@ describe("PreviewCopyJob", () => {
|
||||
);
|
||||
|
||||
expect(getByText(/Job name/i)).toBeInTheDocument();
|
||||
expect(getByText(/Source subscription/i)).toBeInTheDocument();
|
||||
expect(getByText(/Source account/i)).toBeInTheDocument();
|
||||
expect(getByText(/Destination subscription/i)).toBeInTheDocument();
|
||||
expect(getByText(/Destination account/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React, { useEffect } from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { getDefaultJobName } from "../../../CopyJobUtils";
|
||||
import FieldRow from "../Components/FieldRow";
|
||||
@@ -32,19 +32,19 @@ const PreviewCopyJob: React.FC = () => {
|
||||
};
|
||||
return (
|
||||
<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} />
|
||||
</FieldRow>
|
||||
<Stack>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||
<Text data-test="source-subscription-name" className="themeText">
|
||||
{copyJobState.source?.subscription?.displayName}
|
||||
<Text className="bold themeText">{t(Keys.containerCopy.preview.subscriptionLabel)}</Text>
|
||||
<Text data-test="destination-subscription-name" className="themeText">
|
||||
{copyJobState.target?.subscription?.displayName}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text data-test="source-account-name" className="themeText">
|
||||
{copyJobState.source?.account?.name}
|
||||
<Text className="bold themeText">{t(Keys.containerCopy.preview.accountLabel)}</Text>
|
||||
<Text data-test="destination-account-name" className="themeText">
|
||||
{copyJobState.target?.account?.name}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
import { IColumn } from "@fluentui/react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { Keys, t } from "Localization";
|
||||
|
||||
const commonProps = {
|
||||
minWidth: 130,
|
||||
@@ -17,25 +17,25 @@ export const getPreviewCopyJobDetailsListColumns = (): IColumn[] => {
|
||||
return [
|
||||
{
|
||||
key: "sourcedbname",
|
||||
name: ContainerCopyMessages.sourceDatabaseLabel,
|
||||
name: t(Keys.containerCopy.preview.sourceDatabaseLabel),
|
||||
fieldName: "sourceDatabaseName",
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
key: "sourcecolname",
|
||||
name: ContainerCopyMessages.sourceContainerLabel,
|
||||
name: t(Keys.containerCopy.preview.sourceContainerLabel),
|
||||
fieldName: "sourceContainerName",
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
key: "targetdbname",
|
||||
name: ContainerCopyMessages.targetDatabaseLabel,
|
||||
name: t(Keys.containerCopy.preview.targetDatabaseLabel),
|
||||
fieldName: "targetDatabaseName",
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
key: "targetcolname",
|
||||
name: ContainerCopyMessages.targetContainerLabel,
|
||||
name: t(Keys.containerCopy.preview.targetContainerLabel),
|
||||
fieldName: "targetContainerName",
|
||||
...commonProps,
|
||||
},
|
||||
|
||||
+49
-37
@@ -49,11 +49,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -64,11 +64,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -371,11 +371,11 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -386,13 +386,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
target-account
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -693,11 +693,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -708,11 +708,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -1015,13 +1015,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
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>
|
||||
</div>
|
||||
<div
|
||||
@@ -1030,13 +1030,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
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>
|
||||
</div>
|
||||
<div
|
||||
@@ -1337,11 +1337,11 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -1352,7 +1352,13 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -1653,7 +1659,13 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -1662,11 +1674,11 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -1969,11 +1981,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -1984,11 +1996,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -2291,11 +2303,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -2306,11 +2318,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -2613,11 +2625,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source subscription
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-subscription-name"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -2628,11 +2640,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Source account
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="source-account-name"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
|
||||
+29
-31
@@ -5,7 +5,7 @@ import { configContext, Platform } from "../../../../../../ConfigContext";
|
||||
import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
|
||||
import * as useDatabaseAccountsHook from "../../../../../../hooks/useDatabaseAccounts";
|
||||
import { apiType, userContext } from "../../../../../../UserContext";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { Keys, t } from "Localization";
|
||||
import { CopyJobContext } from "../../../../Context/CopyJobContext";
|
||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||
@@ -38,6 +38,12 @@ describe("AccountDropdown", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
displayName: "Test Subscription",
|
||||
@@ -46,13 +52,7 @@ describe("AccountDropdown", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
} as CopyJobContextState;
|
||||
|
||||
const mockCopyJobContextValue = {
|
||||
@@ -129,11 +129,11 @@ describe("AccountDropdown", () => {
|
||||
renderWithContext();
|
||||
|
||||
expect(
|
||||
screen.getByText(`${ContainerCopyMessages.sourceAccountDropdownLabel}:`, { exact: true }),
|
||||
screen.getByText(`${t(Keys.containerCopy.selectAccount.accountDropdownLabel)}:`, { exact: true }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox")).toHaveAttribute(
|
||||
"aria-label",
|
||||
ContainerCopyMessages.sourceAccountDropdownLabel,
|
||||
t(Keys.containerCopy.selectAccount.accountDropdownLabel),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -202,7 +202,7 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const newState = stateUpdateFunction(mockCopyJobState);
|
||||
expect(newState.source.account).toEqual({
|
||||
expect(newState.target.account).toEqual({
|
||||
...mockDatabaseAccount1,
|
||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||
});
|
||||
@@ -226,20 +226,21 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const newState = stateUpdateFunction(mockCopyJobState);
|
||||
expect(newState.source.account).toEqual({
|
||||
expect(newState.target.account).toEqual({
|
||||
...mockDatabaseAccount2,
|
||||
id: normalizeAccountId(mockDatabaseAccount2.id),
|
||||
});
|
||||
});
|
||||
|
||||
it("should keep current account if it exists in the filtered list", async () => {
|
||||
const normalizedAccount1 = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
|
||||
const contextWithSelectedAccount = {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
account: mockDatabaseAccount1,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: normalizedAccount1,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -256,12 +257,9 @@ describe("AccountDropdown", () => {
|
||||
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
||||
expect(newState).toEqual({
|
||||
...contextWithSelectedAccount.copyJobState,
|
||||
source: {
|
||||
...contextWithSelectedAccount.copyJobState.source,
|
||||
account: {
|
||||
...mockDatabaseAccount1,
|
||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||
},
|
||||
target: {
|
||||
...contextWithSelectedAccount.copyJobState.target,
|
||||
account: normalizedAccount1,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -297,8 +295,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: portalAccount,
|
||||
},
|
||||
},
|
||||
@@ -323,8 +321,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: hostedAccount,
|
||||
},
|
||||
},
|
||||
@@ -361,8 +359,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
subscription: null,
|
||||
},
|
||||
} as CopyJobContextState,
|
||||
@@ -376,13 +374,13 @@ describe("AccountDropdown", () => {
|
||||
});
|
||||
|
||||
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 = {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: selectedAccount,
|
||||
},
|
||||
},
|
||||
@@ -409,7 +407,7 @@ describe("AccountDropdown", () => {
|
||||
renderWithContext();
|
||||
|
||||
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", () => {
|
||||
|
||||
+12
-12
@@ -2,11 +2,11 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { Dropdown } from "@fluentui/react";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { Keys, t } from "Localization";
|
||||
import React, { useEffect } from "react";
|
||||
import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
|
||||
import { useDatabaseAccounts } from "../../../../../../hooks/useDatabaseAccounts";
|
||||
import { apiType, userContext } from "../../../../../../UserContext";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../../Context/CopyJobContext";
|
||||
import FieldRow from "../../Components/FieldRow";
|
||||
|
||||
@@ -25,7 +25,7 @@ export const normalizeAccountId = (id: string = "") => {
|
||||
export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||
|
||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||
const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
||||
const sqlApiOnlyAccounts = (allAccounts || [])
|
||||
.filter((account) => apiType(account) === "SQL")
|
||||
@@ -36,11 +36,11 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
|
||||
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
||||
setCopyJobState((prevState) => {
|
||||
if (prevState.source?.account?.id !== newAccount.id) {
|
||||
if (prevState.target?.account?.id !== newAccount.id) {
|
||||
return {
|
||||
...prevState,
|
||||
source: {
|
||||
...prevState.source,
|
||||
target: {
|
||||
...prevState.target,
|
||||
account: newAccount,
|
||||
},
|
||||
};
|
||||
@@ -51,13 +51,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
|
||||
useEffect(() => {
|
||||
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 selectedAccountId = currentAccountId || predefinedAccountId;
|
||||
|
||||
const targetAccount: DatabaseAccount | null =
|
||||
const matchedAccount: DatabaseAccount | null =
|
||||
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
|
||||
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
|
||||
updateCopyJobState(matchedAccount || sqlApiOnlyAccounts[0]);
|
||||
}
|
||||
}, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]);
|
||||
|
||||
@@ -77,13 +77,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
};
|
||||
|
||||
const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0;
|
||||
const selectedAccountId = normalizeAccountId(copyJobState?.source?.account?.id ?? "");
|
||||
const selectedAccountId = normalizeAccountId(copyJobState?.target?.account?.id ?? "");
|
||||
|
||||
return (
|
||||
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}>
|
||||
<FieldRow label={t(Keys.containerCopy.selectAccount.accountDropdownLabel)}>
|
||||
<Dropdown
|
||||
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder}
|
||||
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel}
|
||||
placeholder={t(Keys.containerCopy.selectAccount.accountDropdownPlaceholder)}
|
||||
ariaLabel={t(Keys.containerCopy.selectAccount.accountDropdownLabel)}
|
||||
options={accountOptions}
|
||||
disabled={isAccountDropdownDisabled}
|
||||
required
|
||||
|
||||
+14
-18
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { Keys, t } from "Localization";
|
||||
import { useCopyJobContext } from "../../../../Context/CopyJobContext";
|
||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||
import { MigrationType } from "./MigrationType";
|
||||
@@ -29,7 +29,7 @@ describe("MigrationType", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: { currentScreen: "selectAccount" },
|
||||
@@ -53,9 +53,9 @@ describe("MigrationType", () => {
|
||||
expect(screen.getByRole("radiogroup")).toBeInTheDocument();
|
||||
|
||||
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(onlineRadio).toBeInTheDocument();
|
||||
@@ -65,9 +65,9 @@ describe("MigrationType", () => {
|
||||
it("should render with online mode selected by default", () => {
|
||||
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", {
|
||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
||||
name: t(Keys.containerCopy.migrationType.offline.title),
|
||||
});
|
||||
|
||||
expect(onlineRadio).toBeChecked();
|
||||
@@ -86,9 +86,9 @@ describe("MigrationType", () => {
|
||||
render(<MigrationType />);
|
||||
|
||||
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(onlineRadio).not.toBeChecked();
|
||||
@@ -141,7 +141,7 @@ describe("MigrationType", () => {
|
||||
render(<MigrationType />);
|
||||
|
||||
const offlineRadio = screen.getByRole("radio", {
|
||||
name: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
||||
name: t(Keys.containerCopy.migrationType.offline.title),
|
||||
});
|
||||
fireEvent.click(offlineRadio);
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("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);
|
||||
|
||||
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
|
||||
@@ -198,11 +198,9 @@ describe("MigrationType", () => {
|
||||
render(<MigrationType />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
|
||||
screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.offline.title) }),
|
||||
).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(
|
||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
|
||||
screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.offline.title) }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle null copyJobState gracefully", () => {
|
||||
|
||||
+12
-7
@@ -3,20 +3,20 @@
|
||||
import { ChoiceGroup, IChoiceGroupOption, Stack, Text } from "@fluentui/react";
|
||||
import MarkdownRender from "@nteract/markdown";
|
||||
import { useCopyJobContext } from "Explorer/ContainerCopy/Context/CopyJobContext";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
|
||||
|
||||
interface MigrationTypeProps {}
|
||||
const options: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: CopyJobMigrationType.Offline,
|
||||
text: ContainerCopyMessages.migrationTypeOptions.offline.title,
|
||||
text: t(Keys.containerCopy.migrationType.offline.title),
|
||||
styles: { root: { width: "33%" } },
|
||||
},
|
||||
{
|
||||
key: CopyJobMigrationType.Online,
|
||||
text: ContainerCopyMessages.migrationTypeOptions.online.title,
|
||||
text: t(Keys.containerCopy.migrationType.online.title),
|
||||
styles: { root: { width: "33%" } },
|
||||
},
|
||||
];
|
||||
@@ -47,8 +47,13 @@ export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
|
||||
};
|
||||
|
||||
const selectedKey = copyJobState?.migrationType ?? "";
|
||||
const selectedKeyLowercase = selectedKey.toLowerCase() as keyof typeof ContainerCopyMessages.migrationTypeOptions;
|
||||
const selectedKeyContent = ContainerCopyMessages.migrationTypeOptions[selectedKeyLowercase];
|
||||
const selectedKeyLowercase = selectedKey.toLowerCase() as "offline" | "online";
|
||||
const migrationTypeDescriptionKey =
|
||||
selectedKeyLowercase === "offline"
|
||||
? Keys.containerCopy.migrationType.offline.description
|
||||
: selectedKeyLowercase === "online"
|
||||
? Keys.containerCopy.migrationType.online.description
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Stack data-test="migration-type" className="migrationTypeContainer">
|
||||
@@ -61,14 +66,14 @@ export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
|
||||
styles={choiceGroupStyles}
|
||||
/>
|
||||
</Stack.Item>
|
||||
{selectedKeyContent && (
|
||||
{migrationTypeDescriptionKey && (
|
||||
<Stack.Item styles={{ root: { marginTop: 10 } }}>
|
||||
<Text
|
||||
variant="small"
|
||||
className="migrationTypeDescription"
|
||||
data-test={`migration-type-description-${selectedKeyLowercase}`}
|
||||
>
|
||||
<MarkdownRender source={selectedKeyContent.description} linkTarget="_blank" />
|
||||
<MarkdownRender source={t(migrationTypeDescriptionKey)} linkTarget="_blank" />
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
)}
|
||||
|
||||
-5
@@ -8,14 +8,9 @@ import { SubscriptionDropdown } from "./SubscriptionDropdown";
|
||||
|
||||
jest.mock("../../../../../../hooks/useSubscriptions");
|
||||
jest.mock("../../../../../../UserContext");
|
||||
jest.mock("../../../../ContainerCopyMessages");
|
||||
|
||||
const mockUseSubscriptions = jest.requireMock("../../../../../../hooks/useSubscriptions").useSubscriptions;
|
||||
const mockUserContext = jest.requireMock("../../../../../../UserContext").userContext;
|
||||
const mockContainerCopyMessages = jest.requireMock("../../../../ContainerCopyMessages").default;
|
||||
|
||||
mockContainerCopyMessages.subscriptionDropdownLabel = "Subscription";
|
||||
mockContainerCopyMessages.subscriptionDropdownPlaceholder = "Select a subscription";
|
||||
|
||||
describe("SubscriptionDropdown", () => {
|
||||
let mockExplorer: Explorer;
|
||||
|
||||
+9
-9
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/display-name */
|
||||
import { Dropdown } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React, { useEffect } from "react";
|
||||
import { Subscription } from "../../../../../../Contracts/DataModels";
|
||||
import { useSubscriptions } from "../../../../../../hooks/useSubscriptions";
|
||||
import { userContext } from "../../../../../../UserContext";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../../Context/CopyJobContext";
|
||||
import FieldRow from "../../Components/FieldRow";
|
||||
|
||||
@@ -17,11 +17,11 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
|
||||
|
||||
const updateCopyJobState = (newSubscription: Subscription) => {
|
||||
setCopyJobState((prevState) => {
|
||||
if (prevState.source?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
|
||||
if (prevState.target?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
|
||||
return {
|
||||
...prevState,
|
||||
source: {
|
||||
...prevState.source,
|
||||
target: {
|
||||
...prevState.target,
|
||||
subscription: newSubscription,
|
||||
account: null,
|
||||
},
|
||||
@@ -33,7 +33,7 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
|
||||
|
||||
useEffect(() => {
|
||||
if (subscriptions && subscriptions.length > 0) {
|
||||
const currentSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||
const currentSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||
const predefinedSubscriptionId = userContext.subscriptionId;
|
||||
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 (
|
||||
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}>
|
||||
<FieldRow label={t(Keys.containerCopy.selectAccount.subscriptionDropdownLabel)}>
|
||||
<Dropdown
|
||||
placeholder={ContainerCopyMessages.subscriptionDropdownPlaceholder}
|
||||
ariaLabel={ContainerCopyMessages.subscriptionDropdownLabel}
|
||||
placeholder={t(Keys.containerCopy.selectAccount.subscriptionDropdownPlaceholder)}
|
||||
ariaLabel={t(Keys.containerCopy.selectAccount.subscriptionDropdownLabel)}
|
||||
data-test="subscription-dropdown"
|
||||
options={subscriptionOptions}
|
||||
required
|
||||
|
||||
+8
-8
@@ -30,18 +30,18 @@ describe("SelectAccount", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
source: {
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
target: {
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: { currentScreen: "selectAccount" },
|
||||
@@ -68,7 +68,7 @@ describe("SelectAccount", () => {
|
||||
expect(container.firstChild).toHaveAttribute("data-test", "Panel: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("account-dropdown")).toBeInTheDocument();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { AccountDropdown } from "./Components/AccountDropdown";
|
||||
import { MigrationType } from "./Components/MigrationType";
|
||||
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
|
||||
@@ -8,7 +8,7 @@ import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
|
||||
const SelectAccount = React.memo(() => {
|
||||
return (
|
||||
<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 />
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
||||
<span
|
||||
class="themeText css-110"
|
||||
>
|
||||
Please select a source account from which to copy.
|
||||
Please select a destination account to copy to.
|
||||
</span>
|
||||
<div
|
||||
data-testid="subscription-dropdown"
|
||||
|
||||
+19
-19
@@ -7,19 +7,9 @@ import { dropDownChangeHandler } from "./DropDownChangeHandler";
|
||||
const createMockInitialState = (): CopyJobContextState => ({
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
source: {
|
||||
subscription: {
|
||||
subscriptionId: "source-sub-id",
|
||||
displayName: "Source Subscription",
|
||||
state: "Enabled",
|
||||
subscriptionPolicies: {
|
||||
locationPlacementId: "test",
|
||||
quotaId: "test",
|
||||
spendingLimit: "Off",
|
||||
},
|
||||
authorizationSource: "test",
|
||||
},
|
||||
subscriptionId: "source-sub-id",
|
||||
account: {
|
||||
id: "source-account-id",
|
||||
name: "source-account",
|
||||
@@ -50,7 +40,17 @@ const createMockInitialState = (): CopyJobContextState => ({
|
||||
containerId: "source-container",
|
||||
},
|
||||
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: {
|
||||
id: "target-account-id",
|
||||
name: "target-account",
|
||||
@@ -169,7 +169,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.source.databaseId).toBe("new-source-db");
|
||||
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.target).toEqual(initialState.target);
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.jobName).toBe(initialState.jobName);
|
||||
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.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.target).toEqual(initialState.target);
|
||||
});
|
||||
@@ -215,7 +215,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.target.databaseId).toBe("new-target-db");
|
||||
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.source).toEqual(initialState.source);
|
||||
});
|
||||
@@ -227,7 +227,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.jobName).toBe(initialState.jobName);
|
||||
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.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.source).toEqual(initialState.source);
|
||||
});
|
||||
|
||||
+27
-28
@@ -15,15 +15,6 @@ jest.mock("../../../../../hooks/useDataContainers", () => ({
|
||||
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", () => ({
|
||||
dropDownChangeHandler: jest.fn(() => () => jest.fn()),
|
||||
}));
|
||||
@@ -73,7 +64,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: { subscriptionId: "test-subscription-id" },
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
name: "test-account",
|
||||
@@ -82,7 +73,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
containerId: "container1",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: { subscriptionId: "test-subscription-id" },
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
name: "test-account",
|
||||
@@ -90,7 +81,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
databaseId: "db2",
|
||||
containerId: "container2",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
};
|
||||
|
||||
const mockMemoizedData = {
|
||||
@@ -124,22 +115,26 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
describe("Component Rendering", () => {
|
||||
it("should render without crashing", () => {
|
||||
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", () => {
|
||||
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", () => {
|
||||
renderWithContext(<SelectSourceAndTargetContainers />);
|
||||
expect(screen.getByText("Source Container")).toBeInTheDocument();
|
||||
expect(screen.getByText("Source container")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render target container section", () => {
|
||||
renderWithContext(<SelectSourceAndTargetContainers />);
|
||||
expect(screen.getByText("Target Container")).toBeInTheDocument();
|
||||
expect(screen.getByText("Destination container")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should return null when source is not available", () => {
|
||||
@@ -238,14 +233,14 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
describe("Component Props", () => {
|
||||
it("should pass showAddCollectionPanel to DatabaseContainerSection", () => {
|
||||
renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />);
|
||||
expect(screen.getByText("Target Container")).toBeInTheDocument();
|
||||
expect(screen.getByText("Destination container")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render without showAddCollectionPanel prop", () => {
|
||||
renderWithContext(<SelectSourceAndTargetContainers />);
|
||||
|
||||
expect(screen.getByText("Source Container")).toBeInTheDocument();
|
||||
expect(screen.getByText("Target Container")).toBeInTheDocument();
|
||||
expect(screen.getByText("Source container")).toBeInTheDocument();
|
||||
expect(screen.getByText("Destination container")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,13 +305,13 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
it("should pass correct props to source DatabaseContainerSection", () => {
|
||||
renderWithContext(<SelectSourceAndTargetContainers />);
|
||||
|
||||
expect(screen.getByText("Source Container")).toBeInTheDocument();
|
||||
expect(screen.getByText("Source container")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should pass correct props to target DatabaseContainerSection", () => {
|
||||
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", () => {
|
||||
@@ -329,7 +324,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
} as ReturnType<typeof useSourceAndTargetData>);
|
||||
|
||||
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", () => {
|
||||
@@ -342,7 +337,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
} as ReturnType<typeof useSourceAndTargetData>);
|
||||
|
||||
renderWithContext(<SelectSourceAndTargetContainers />);
|
||||
expect(screen.getByText("Target Container")).toBeInTheDocument();
|
||||
expect(screen.getByText("Destination container")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -353,7 +348,9 @@ describe("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", () => {
|
||||
@@ -421,7 +418,9 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
it("should apply correct spacing tokens", () => {
|
||||
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", () => {
|
||||
renderWithContext(<SelectSourceAndTargetContainers />);
|
||||
|
||||
const description = screen.getByText("Select source and target containers for migration");
|
||||
const sourceSection = screen.getByText("Source Container");
|
||||
const targetSection = screen.getByText("Target Container");
|
||||
const description = screen.getByText("Please select a source container and a destination container to copy to.");
|
||||
const sourceSection = screen.getByText("Source container");
|
||||
const targetSection = screen.getByText("Destination container");
|
||||
|
||||
expect(description).toBeInTheDocument();
|
||||
expect(sourceSection).toBeInTheDocument();
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { DatabaseModel } from "Contracts/DataModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { useDatabases } from "../../../../../hooks/useDatabases";
|
||||
import { useDataContainers } from "../../../../../hooks/useDataContainers";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { DatabaseContainerSection } from "./components/DatabaseContainerSection";
|
||||
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
|
||||
@@ -52,9 +52,9 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
||||
className="selectSourceAndTargetContainers"
|
||||
tokens={{ childrenGap: 25 }}
|
||||
>
|
||||
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
|
||||
<span className="themeText">{t(Keys.containerCopy.selectContainers.description)}</span>
|
||||
<DatabaseContainerSection
|
||||
heading={ContainerCopyMessages.sourceContainerSubHeading}
|
||||
heading={t(Keys.containerCopy.selectContainers.sourceContainerSubHeading)}
|
||||
databaseOptions={sourceDatabaseOptions}
|
||||
selectedDatabase={source?.databaseId}
|
||||
databaseDisabled={false}
|
||||
@@ -66,7 +66,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
|
||||
sectionType="source"
|
||||
/>
|
||||
<DatabaseContainerSection
|
||||
heading={ContainerCopyMessages.targetContainerSubHeading}
|
||||
heading={t(Keys.containerCopy.selectContainers.targetContainerSubHeading)}
|
||||
databaseOptions={targetDatabaseOptions}
|
||||
selectedDatabase={target?.databaseId}
|
||||
databaseDisabled={false}
|
||||
|
||||
+53
-31
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { Keys, t } from "Localization";
|
||||
import { DatabaseContainerSectionProps, DropdownOptionType } from "../../../../Types/CopyJobTypes";
|
||||
import { DatabaseContainerSection } from "./DatabaseContainerSection";
|
||||
|
||||
@@ -60,11 +60,14 @@ describe("DatabaseContainerSection", () => {
|
||||
render(<DatabaseContainerSection {...defaultProps} />);
|
||||
|
||||
const databaseDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.databaseDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
});
|
||||
|
||||
expect(databaseDropdown).toBeInTheDocument();
|
||||
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel);
|
||||
expect(databaseDropdown).toHaveAttribute(
|
||||
"aria-label",
|
||||
t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
);
|
||||
expect(databaseDropdown).not.toBeDisabled();
|
||||
});
|
||||
|
||||
@@ -72,30 +75,35 @@ describe("DatabaseContainerSection", () => {
|
||||
render(<DatabaseContainerSection {...defaultProps} />);
|
||||
|
||||
const containerDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.containerDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
});
|
||||
|
||||
expect(containerDropdown).toBeInTheDocument();
|
||||
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel);
|
||||
expect(containerDropdown).toHaveAttribute(
|
||||
"aria-label",
|
||||
t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
);
|
||||
expect(containerDropdown).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("renders database label correctly", () => {
|
||||
render(<DatabaseContainerSection {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(`${ContainerCopyMessages.databaseDropdownLabel}:`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}:`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders container label correctly", () => {
|
||||
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", () => {
|
||||
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", () => {
|
||||
@@ -107,7 +115,7 @@ describe("DatabaseContainerSection", () => {
|
||||
const createButton = container.querySelector(".create-container-link-btn");
|
||||
|
||||
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} />);
|
||||
|
||||
const databaseDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.databaseDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
});
|
||||
|
||||
expect(databaseDropdown).toHaveAttribute("aria-disabled", "true");
|
||||
@@ -136,7 +144,7 @@ describe("DatabaseContainerSection", () => {
|
||||
render(<DatabaseContainerSection {...propsWithDisabledContainer} />);
|
||||
|
||||
const containerDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.containerDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
});
|
||||
|
||||
expect(containerDropdown).toHaveAttribute("aria-disabled", "true");
|
||||
@@ -152,10 +160,10 @@ describe("DatabaseContainerSection", () => {
|
||||
render(<DatabaseContainerSection {...propsWithFalsyDisabled} />);
|
||||
|
||||
const databaseDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.databaseDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
});
|
||||
const containerDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.containerDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
});
|
||||
|
||||
expect(databaseDropdown).not.toHaveAttribute("aria-disabled", "true");
|
||||
@@ -167,21 +175,27 @@ describe("DatabaseContainerSection", () => {
|
||||
it("calls databaseOnChange when database dropdown selection changes", () => {
|
||||
render(<DatabaseContainerSection {...defaultProps} />);
|
||||
const databaseDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.databaseDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
});
|
||||
|
||||
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", () => {
|
||||
render(<DatabaseContainerSection {...defaultProps} />);
|
||||
const containerDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.containerDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
});
|
||||
|
||||
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", () => {
|
||||
@@ -192,7 +206,7 @@ describe("DatabaseContainerSection", () => {
|
||||
|
||||
render(<DatabaseContainerSection {...propsWithCreateHandler} />);
|
||||
|
||||
const createButton = screen.getByText(ContainerCopyMessages.createContainerButtonLabel);
|
||||
const createButton = screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel));
|
||||
fireEvent.click(createButton);
|
||||
|
||||
expect(mockHandleOnDemandCreateContainer).toHaveBeenCalledTimes(1);
|
||||
@@ -235,10 +249,10 @@ describe("DatabaseContainerSection", () => {
|
||||
render(<DatabaseContainerSection {...propsWithEmptyOptions} />);
|
||||
|
||||
const databaseDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.databaseDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
});
|
||||
const containerDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.containerDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
});
|
||||
|
||||
expect(databaseDropdown).toBeInTheDocument();
|
||||
@@ -251,24 +265,30 @@ describe("DatabaseContainerSection", () => {
|
||||
render(<DatabaseContainerSection {...defaultProps} />);
|
||||
|
||||
const databaseDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.databaseDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
});
|
||||
const containerDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.containerDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
});
|
||||
|
||||
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel);
|
||||
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel);
|
||||
expect(databaseDropdown).toHaveAttribute(
|
||||
"aria-label",
|
||||
t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
);
|
||||
expect(containerDropdown).toHaveAttribute(
|
||||
"aria-label",
|
||||
t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
);
|
||||
});
|
||||
|
||||
it("has proper required attributes for dropdowns", () => {
|
||||
render(<DatabaseContainerSection {...defaultProps} />);
|
||||
|
||||
const databaseDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.databaseDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
|
||||
});
|
||||
const containerDropdown = screen.getByRole("combobox", {
|
||||
name: ContainerCopyMessages.containerDropdownLabel,
|
||||
name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
|
||||
});
|
||||
|
||||
expect(databaseDropdown).toHaveAttribute("aria-required", "true");
|
||||
@@ -278,8 +298,8 @@ describe("DatabaseContainerSection", () => {
|
||||
it("maintains proper label associations", () => {
|
||||
render(<DatabaseContainerSection {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(`${ContainerCopyMessages.databaseDropdownLabel}:`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${ContainerCopyMessages.containerDropdownLabel}:`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}:`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.containerDropdownLabel)}:`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,7 +319,9 @@ describe("DatabaseContainerSection", () => {
|
||||
render(<DatabaseContainerSection {...minimalProps} />);
|
||||
|
||||
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", () => {
|
||||
@@ -366,7 +388,7 @@ describe("DatabaseContainerSection", () => {
|
||||
|
||||
const { container } = render(<DatabaseContainerSection {...propsWithCreateHandler} />);
|
||||
|
||||
const createButton = screen.getByText(ContainerCopyMessages.createContainerButtonLabel);
|
||||
const createButton = screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel));
|
||||
expect(createButton).toBeInTheDocument();
|
||||
|
||||
const containerSection = container.querySelector(".databaseContainerSection");
|
||||
@@ -381,7 +403,7 @@ describe("DatabaseContainerSection", () => {
|
||||
|
||||
render(<DatabaseContainerSection {...propsWithCreateHandler} />);
|
||||
|
||||
expect(screen.getByText(ContainerCopyMessages.createContainerButtonLabel)).toBeInTheDocument();
|
||||
expect(screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
import { ActionButton, Dropdown, Stack } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
|
||||
import { DatabaseContainerSectionProps } from "../../../../Types/CopyJobTypes";
|
||||
import FieldRow from "../../Components/FieldRow";
|
||||
|
||||
@@ -19,10 +19,10 @@ export const DatabaseContainerSection = ({
|
||||
}: DatabaseContainerSectionProps) => (
|
||||
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
|
||||
<label className="subHeading">{heading}</label>
|
||||
<FieldRow label={ContainerCopyMessages.databaseDropdownLabel}>
|
||||
<FieldRow label={t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}>
|
||||
<Dropdown
|
||||
placeholder={ContainerCopyMessages.databaseDropdownPlaceholder}
|
||||
ariaLabel={ContainerCopyMessages.databaseDropdownLabel}
|
||||
placeholder={t(Keys.containerCopy.selectContainers.databaseDropdownPlaceholder)}
|
||||
ariaLabel={t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}
|
||||
options={databaseOptions}
|
||||
required
|
||||
disabled={!!databaseDisabled}
|
||||
@@ -31,11 +31,11 @@ export const DatabaseContainerSection = ({
|
||||
data-test={`${sectionType}-databaseDropdown`}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
|
||||
<FieldRow label={t(Keys.containerCopy.selectContainers.containerDropdownLabel)}>
|
||||
<Stack>
|
||||
<Dropdown
|
||||
placeholder={ContainerCopyMessages.containerDropdownPlaceholder}
|
||||
ariaLabel={ContainerCopyMessages.containerDropdownLabel}
|
||||
placeholder={t(Keys.containerCopy.selectContainers.containerDropdownPlaceholder)}
|
||||
ariaLabel={t(Keys.containerCopy.selectContainers.containerDropdownLabel)}
|
||||
options={containerOptions}
|
||||
required
|
||||
disabled={!!containerDisabled}
|
||||
@@ -49,7 +49,7 @@ export const DatabaseContainerSection = ({
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
onClick={() => handleOnDemandCreateContainer()}
|
||||
>
|
||||
{ContainerCopyMessages.createContainerButtonLabel}
|
||||
{t(Keys.containerCopy.selectContainers.createContainerButtonLabel)}
|
||||
</ActionButton>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
+3
-3
@@ -69,15 +69,15 @@ describe("useSourceAndTargetData", () => {
|
||||
const mockCopyJobState: CopyJobContextState = {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "source-subscription-id",
|
||||
account: mockSourceAccount,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "target-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockTargetAccount,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
|
||||
@@ -86,13 +86,13 @@ describe("useCopyJobNavigation", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: { subscriptionId: "source-sub-id" } as any,
|
||||
subscriptionId: "source-sub-id",
|
||||
account: { id: "source-account-id", name: "Account-1" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "target-sub-id",
|
||||
subscription: { subscriptionId: "target-sub-id" } as any,
|
||||
account: { id: "target-account-id", name: "Account-2" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
|
||||
+17
-17
@@ -142,14 +142,14 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: { subscriptionId: "test-sub" } as any,
|
||||
subscriptionId: "test-sub",
|
||||
account: { name: "test-account" } as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
subscription: { subscriptionId: "test-sub" } as any,
|
||||
account: { name: "test-account" } as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -171,14 +171,14 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null as any,
|
||||
account: { name: "test-account" } as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
subscription: null as any,
|
||||
account: { name: "test-account" } as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -210,13 +210,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -240,13 +240,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -288,13 +288,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "valid-job-name_123",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
@@ -318,13 +318,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "invalid job name with spaces!",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
@@ -348,13 +348,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
|
||||
@@ -36,7 +36,7 @@ function useCreateCopyJobScreensList(goBack: () => void): Screen[] {
|
||||
component: <SelectAccount />,
|
||||
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",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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", () => {
|
||||
const createMockJob = (overrides: Partial<CopyJobType> = {}): CopyJobType =>
|
||||
({
|
||||
@@ -301,8 +279,8 @@ describe("CopyJobActionMenu", () => {
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"",
|
||||
"",
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
@@ -358,8 +336,8 @@ describe("CopyJobActionMenu", () => {
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"",
|
||||
"",
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
@@ -402,8 +380,8 @@ describe("CopyJobActionMenu", () => {
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"",
|
||||
"",
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
@@ -433,8 +411,8 @@ describe("CopyJobActionMenu", () => {
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"",
|
||||
"",
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
@@ -849,8 +827,8 @@ describe("CopyJobActionMenu", () => {
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action", // title
|
||||
null, // subText
|
||||
"", // title
|
||||
"", // subText
|
||||
"Confirm", // confirmLabel
|
||||
expect.any(Function), // onOk
|
||||
"Cancel", // cancelLabel
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { useDialog } from "../../../Controls/Dialog";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
|
||||
@@ -49,11 +49,11 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
ContainerCopyMessages.MonitorJobs.dialog.heading,
|
||||
null,
|
||||
ContainerCopyMessages.MonitorJobs.dialog.confirmButtonText,
|
||||
"",
|
||||
"",
|
||||
t(Keys.common.confirm),
|
||||
() => handleClick(job, action, setUpdatingJobAction),
|
||||
ContainerCopyMessages.MonitorJobs.dialog.cancelButtonText,
|
||||
t(Keys.common.cancel),
|
||||
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 = [
|
||||
{
|
||||
key: CopyJobActions.pause,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
||||
text: t(Keys.containerCopy.monitorJobs.actions.pause),
|
||||
iconProps: { iconName: "Pause" },
|
||||
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.cancel,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
||||
text: t(Keys.common.cancel),
|
||||
iconProps: { iconName: "Cancel" },
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
|
||||
disabled: isThisJobUpdating,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.resume,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
||||
text: t(Keys.containerCopy.monitorJobs.actions.resume),
|
||||
iconProps: { iconName: "Play" },
|
||||
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating,
|
||||
@@ -101,7 +101,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
if ((job.Mode ?? "").toLowerCase() === CopyJobMigrationType.Online) {
|
||||
filteredItems.push({
|
||||
key: CopyJobActions.complete,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||
text: t(Keys.containerCopy.monitorJobs.actions.complete),
|
||||
iconProps: { iconName: "CheckMark" },
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
||||
disabled: isThisJobUpdating,
|
||||
@@ -124,8 +124,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
|
||||
menuIconProps={{ iconName: "", className: "hidden" }}
|
||||
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
ariaLabel={t(Keys.containerCopy.monitorJobs.columns.actions)}
|
||||
title={t(Keys.containerCopy.monitorJobs.columns.actions)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IColumn } from "@fluentui/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render } from "@testing-library/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import { getColumns } from "./CopyJobColumns";
|
||||
@@ -79,14 +79,14 @@ describe("CopyJobColumns", () => {
|
||||
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);
|
||||
|
||||
expect(columns[0].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime);
|
||||
expect(columns[1].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.name);
|
||||
expect(columns[2].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.mode);
|
||||
expect(columns[3].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.completionPercentage);
|
||||
expect(columns[4].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.status);
|
||||
expect(columns[0].name).toBe(t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime));
|
||||
expect(columns[1].name).toBe(t(Keys.containerCopy.monitorJobs.columns.name));
|
||||
expect(columns[2].name).toBe(t(Keys.containerCopy.monitorJobs.columns.mode));
|
||||
expect(columns[3].name).toBe(t(Keys.containerCopy.monitorJobs.columns.completionPercentage));
|
||||
expect(columns[4].name).toBe(t(Keys.containerCopy.monitorJobs.columns.status));
|
||||
expect(columns[5].name).toBe("");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IColumn } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
|
||||
@@ -13,7 +13,7 @@ export const getColumns = (
|
||||
): IColumn[] => [
|
||||
{
|
||||
key: "LastUpdatedTime",
|
||||
name: ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime,
|
||||
name: t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime),
|
||||
fieldName: "LastUpdatedTime",
|
||||
minWidth: 140,
|
||||
maxWidth: 300,
|
||||
@@ -24,7 +24,7 @@ export const getColumns = (
|
||||
},
|
||||
{
|
||||
key: "Name",
|
||||
name: ContainerCopyMessages.MonitorJobs.Columns.name,
|
||||
name: t(Keys.containerCopy.monitorJobs.columns.name),
|
||||
fieldName: "Name",
|
||||
minWidth: 140,
|
||||
maxWidth: 300,
|
||||
@@ -36,7 +36,7 @@ export const getColumns = (
|
||||
},
|
||||
{
|
||||
key: "Mode",
|
||||
name: ContainerCopyMessages.MonitorJobs.Columns.mode,
|
||||
name: t(Keys.containerCopy.monitorJobs.columns.mode),
|
||||
fieldName: "Mode",
|
||||
minWidth: 90,
|
||||
maxWidth: 200,
|
||||
@@ -47,7 +47,7 @@ export const getColumns = (
|
||||
},
|
||||
{
|
||||
key: "CompletionPercentage",
|
||||
name: ContainerCopyMessages.MonitorJobs.Columns.completionPercentage,
|
||||
name: t(Keys.containerCopy.monitorJobs.columns.completionPercentage),
|
||||
fieldName: "CompletionPercentage",
|
||||
minWidth: 110,
|
||||
maxWidth: 200,
|
||||
@@ -59,7 +59,7 @@ export const getColumns = (
|
||||
},
|
||||
{
|
||||
key: "CopyJobStatus",
|
||||
name: ContainerCopyMessages.MonitorJobs.Columns.status,
|
||||
name: t(Keys.containerCopy.monitorJobs.columns.status),
|
||||
fieldName: "Status",
|
||||
minWidth: 130,
|
||||
maxWidth: 200,
|
||||
|
||||
@@ -13,22 +13,6 @@ jest.mock("./CopyJobStatusWithIcon", () => {
|
||||
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", () => {
|
||||
const mockBasicJob: CopyJobType = {
|
||||
ID: "test-job-1",
|
||||
@@ -102,8 +86,8 @@ describe("CopyJobDetails", () => {
|
||||
expect(screen.getByText("Date & time")).toBeInTheDocument();
|
||||
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Source Account")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceAccount")).toBeInTheDocument();
|
||||
expect(screen.getByText("Destination account")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetAccount")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Mode")).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-target-db-with-hyphens")).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} />);
|
||||
|
||||
const dateTimeHeading = screen.getByText("Date & time");
|
||||
const sourceAccountHeading = screen.getByText("Source Account");
|
||||
const destinationAccountHeading = screen.getByText("Destination account");
|
||||
const modeHeading = screen.getByText("Mode");
|
||||
|
||||
expect(dateTimeHeading).toHaveClass("bold");
|
||||
expect(sourceAccountHeading).toHaveClass("bold");
|
||||
expect(destinationAccountHeading).toHaveClass("bold");
|
||||
expect(modeHeading).toHaveClass("bold");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React, { memo } from "react";
|
||||
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
|
||||
@@ -31,31 +31,31 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
|
||||
return [
|
||||
{
|
||||
key: "sourcedbcol",
|
||||
name: ContainerCopyMessages.sourceDatabaseLabel,
|
||||
name: t(Keys.containerCopy.preview.sourceDatabaseLabel),
|
||||
fieldName: "sourceDatabaseName",
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
key: "sourcecol",
|
||||
name: ContainerCopyMessages.sourceContainerLabel,
|
||||
name: t(Keys.containerCopy.preview.sourceContainerLabel),
|
||||
fieldName: "sourceContainerName",
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
key: "targetdbcol",
|
||||
name: ContainerCopyMessages.targetDatabaseLabel,
|
||||
name: t(Keys.containerCopy.preview.targetDatabaseLabel),
|
||||
fieldName: "targetDatabaseName",
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
key: "targetcol",
|
||||
name: ContainerCopyMessages.targetContainerLabel,
|
||||
name: t(Keys.containerCopy.preview.targetContainerLabel),
|
||||
fieldName: "targetContainerName",
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
key: "statuscol",
|
||||
name: ContainerCopyMessages.MonitorJobs.Columns.status,
|
||||
name: t(Keys.containerCopy.monitorJobs.columns.status),
|
||||
fieldName: "jobStatus",
|
||||
onRender: ({ jobStatus }: { jobStatus: CopyJobStatusType }) => <CopyJobStatusWithIcon status={jobStatus} />,
|
||||
...commonProps,
|
||||
@@ -92,7 +92,7 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
||||
{job.Error ? (
|
||||
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
|
||||
<Text className="bold themeText" style={sectionCss.headingText}>
|
||||
{ContainerCopyMessages.errorTitle}
|
||||
{t(Keys.containerCopy.jobDetails.errorTitle)}
|
||||
</Text>
|
||||
<Text as="pre" style={errorMessageStyle}>
|
||||
{job.Error.message}
|
||||
@@ -102,15 +102,15 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
||||
<Stack.Item data-testid="selectedcollection-stack">
|
||||
<Stack tokens={{ childrenGap: 15 }}>
|
||||
<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>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={sectionCss.verticalAlign}>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
|
||||
<Text className="bold themeText">{t(Keys.containerCopy.preview.accountLabel)}</Text>
|
||||
<Text className="themeText">{job.Destination?.remoteAccountName}</Text>
|
||||
</Stack.Item>
|
||||
<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>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
|
||||
const iconClass = mergeStyles({
|
||||
@@ -30,12 +30,25 @@ const statusIconColors: Partial<Record<CopyJobStatusType, string>> = {
|
||||
[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 {
|
||||
status: CopyJobStatusType;
|
||||
}
|
||||
|
||||
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 = [
|
||||
CopyJobStatusType.Running,
|
||||
|
||||
@@ -3,9 +3,9 @@ jest.mock("../../Actions/CopyJobActions");
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import * as Actions from "../../Actions/CopyJobActions";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import CopyJobsNotFound from "./CopyJobs.NotFound";
|
||||
|
||||
describe("CopyJobsNotFound", () => {
|
||||
@@ -22,10 +22,10 @@ describe("CopyJobsNotFound", () => {
|
||||
const image = container.querySelector(".notFoundContainer .ms-Image");
|
||||
expect(image).toBeInTheDocument();
|
||||
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", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
|
||||
});
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass("createCopyJobButton");
|
||||
@@ -45,7 +45,7 @@ describe("CopyJobsNotFound", () => {
|
||||
render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
@@ -58,11 +58,11 @@ describe("CopyJobsNotFound", () => {
|
||||
render(<CopyJobsNotFound explorer={mockExplorer} />);
|
||||
|
||||
const button = screen.getByRole("button", {
|
||||
name: ContainerCopyMessages.createCopyJobButtonText,
|
||||
name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
|
||||
});
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ActionButton, Image } from "@fluentui/react";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg";
|
||||
import * as Actions from "../../Actions/CopyJobActions";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
|
||||
interface CopyJobsNotFoundProps {
|
||||
explorer: Explorer;
|
||||
@@ -12,14 +12,14 @@ interface CopyJobsNotFoundProps {
|
||||
const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => {
|
||||
return (
|
||||
<div className="notFoundContainer flexContainer centerContent">
|
||||
<Image src={CopyJobIcon} alt={ContainerCopyMessages.noCopyJobsTitle} width={100} height={100} />
|
||||
<h4 className="noCopyJobsMessage">{ContainerCopyMessages.noCopyJobsTitle}</h4>
|
||||
<Image src={CopyJobIcon} alt={t(Keys.containerCopy.noCopyJobs.title)} width={100} height={100} />
|
||||
<h4 className="noCopyJobsMessage">{t(Keys.containerCopy.noCopyJobs.title)}</h4>
|
||||
<ActionButton
|
||||
allowDisabledFocus
|
||||
className="createCopyJobButton"
|
||||
onClick={() => Actions.openCreateCopyJobPanel(explorer)}
|
||||
>
|
||||
{ContainerCopyMessages.createCopyJobButtonText}
|
||||
{t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText)}
|
||||
</ActionButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -55,15 +55,15 @@ export interface DatabaseContainerSectionProps {
|
||||
export interface CopyJobContextState {
|
||||
jobName: string;
|
||||
migrationType: CopyJobMigrationType;
|
||||
sourceReadAccessFromTarget?: boolean;
|
||||
sourceReadWriteAccessFromTarget?: boolean;
|
||||
source: {
|
||||
subscription: Subscription | null;
|
||||
subscriptionId: string;
|
||||
account: DatabaseAccount | null;
|
||||
databaseId: string;
|
||||
containerId: string;
|
||||
};
|
||||
target: {
|
||||
subscriptionId: string;
|
||||
subscription: Subscription | null;
|
||||
account: DatabaseAccount | null;
|
||||
databaseId: string;
|
||||
containerId: string;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Stack,
|
||||
TextField,
|
||||
} 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 * as React from "react";
|
||||
import { isFullTextSearchPreviewFeaturesEnabled } from "Utils/CapabilityUtils";
|
||||
@@ -25,6 +25,7 @@ export interface FullTextPoliciesComponentProps {
|
||||
discardChanges?: boolean;
|
||||
onChangesDiscarded?: () => void;
|
||||
englishOnly?: boolean;
|
||||
targetAccountOverride?: AccountOverride;
|
||||
}
|
||||
|
||||
export interface FullTextPolicyData {
|
||||
@@ -206,6 +207,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
discardChanges,
|
||||
onChangesDiscarded,
|
||||
englishOnly,
|
||||
targetAccountOverride,
|
||||
}): JSX.Element => {
|
||||
const getFullTextPathError = (path: string, index?: number): string => {
|
||||
let error = "";
|
||||
@@ -236,7 +238,9 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
|
||||
const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy));
|
||||
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(() => {
|
||||
@@ -307,7 +311,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getFullTextLanguageOptions(englishOnly)}
|
||||
options={getFullTextLanguageOptions(englishOnly, targetAccountOverride)}
|
||||
selectedKey={defaultLanguage}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
setDefaultLanguage(option.key as never)
|
||||
@@ -352,7 +356,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getFullTextLanguageOptions(englishOnly)}
|
||||
options={getFullTextLanguageOptions(englishOnly, targetAccountOverride)}
|
||||
selectedKey={fullTextPolicy.language}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onFullTextPathPolicyChange(index, option)
|
||||
@@ -395,8 +399,12 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
);
|
||||
};
|
||||
|
||||
export const getFullTextLanguageOptions = (englishOnly?: boolean): IDropdownOption[] => {
|
||||
const multiLanguageSupportEnabled: boolean = isFullTextSearchPreviewFeaturesEnabled() && !englishOnly;
|
||||
export const getFullTextLanguageOptions = (
|
||||
englishOnly?: boolean,
|
||||
targetAccountOverride?: AccountOverride,
|
||||
): IDropdownOption[] => {
|
||||
const multiLanguageSupportEnabled: boolean =
|
||||
isFullTextSearchPreviewFeaturesEnabled(targetAccountOverride) && !englishOnly;
|
||||
const fullTextLanguageOptions: IDropdownOption[] = [
|
||||
{
|
||||
key: "en-US",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -223,7 +223,11 @@ export default class Explorer {
|
||||
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 = {
|
||||
linkProps: {
|
||||
linkText: "Learn more",
|
||||
@@ -245,7 +249,7 @@ export default class Explorer {
|
||||
useDialog.getState().closeDialog();
|
||||
|
||||
try {
|
||||
await update(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, {
|
||||
await update(subscriptionId, resourceGroup, accountName, {
|
||||
properties: {
|
||||
enableAnalyticalStorage: true,
|
||||
},
|
||||
@@ -254,7 +258,9 @@ export default class Explorer {
|
||||
clearInProgressMessage();
|
||||
logConsoleInfo("Enabled Azure Synapse Link for this account");
|
||||
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
|
||||
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
|
||||
if (!targetAccountOverride) {
|
||||
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
|
||||
}
|
||||
} catch (error) {
|
||||
clearInProgressMessage();
|
||||
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 React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -12,4 +13,58 @@ describe("AddCollectionPanel", () => {
|
||||
const wrapper = shallow(<AddCollectionPanel {...props} />);
|
||||
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 { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { AccountOverride } from "Contracts/DataModels";
|
||||
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import {
|
||||
@@ -67,6 +68,8 @@ export interface AddCollectionPanelProps {
|
||||
isQuickstart?: boolean;
|
||||
isCopyJobFlow?: boolean;
|
||||
onSubmitSuccess?: (collectionData: { databaseId: string; collectionId: string }) => void;
|
||||
targetAccountOverride?: AccountOverride;
|
||||
externalDatabaseOptions?: IDropdownOption[];
|
||||
}
|
||||
|
||||
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
|
||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||
messageType="info"
|
||||
@@ -644,53 +647,57 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Checkbox
|
||||
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
checked={this.state.enableDedicatedThroughput}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
root: {
|
||||
selectors: {
|
||||
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" },
|
||||
{!isServerlessAccount(this.props.targetAccountOverride) &&
|
||||
!this.state.createNewDatabase &&
|
||||
this.isSelectedDatabaseSharedThroughput() && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Checkbox
|
||||
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
checked={this.state.enableDedicatedThroughput}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
root: {
|
||||
selectors: {
|
||||
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ enableDedicatedThroughput: isChecked })
|
||||
}
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
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, {
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ enableDedicatedThroughput: isChecked })
|
||||
}
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||
showFreeTierExceedThroughputTooltip={
|
||||
isFreeTierAccount(this.props.targetAccountOverride) && !isFirstResourceCreated
|
||||
}
|
||||
isDatabase={false}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={isFreeTierAccount()}
|
||||
isFreeTier={isFreeTierAccount(this.props.targetAccountOverride)}
|
||||
isQuickstart={this.props.isQuickstart}
|
||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||
@@ -767,7 +774,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
disabled={!isSynapseLinkEnabled(this.props.targetAccountOverride)}
|
||||
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
|
||||
aria-checked={this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
@@ -782,7 +789,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
disabled={!isSynapseLinkEnabled(this.props.targetAccountOverride)}
|
||||
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
|
||||
aria-checked={!this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
@@ -796,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{!isSynapseLinkEnabled() && (
|
||||
{!isSynapseLinkEnabled(this.props.targetAccountOverride) && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
|
||||
@@ -814,7 +821,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Text>
|
||||
<DefaultButton
|
||||
text={t(Keys.panes.addCollection.enable)}
|
||||
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
|
||||
onClick={() => this.props.explorer.openEnableSynapseLinkDialog(this.props.targetAccountOverride)}
|
||||
style={{ height: 27, width: 80 }}
|
||||
styles={{ label: { fontSize: 12 } }}
|
||||
/>
|
||||
@@ -865,6 +872,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<FullTextPoliciesComponent
|
||||
targetAccountOverride={this.props.targetAccountOverride}
|
||||
fullTextPolicy={this.state.fullTextPolicy}
|
||||
onFullTextPathChange={(
|
||||
fullTextPolicy: DataModels.FullTextPolicy,
|
||||
@@ -1000,6 +1008,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private getDatabaseOptions(): IDropdownOption[] {
|
||||
if (this.props.externalDatabaseOptions) {
|
||||
return this.props.externalDatabaseOptions;
|
||||
}
|
||||
return useDatabases.getState().databases?.map((database) => ({
|
||||
key: database.id(),
|
||||
text: database.id(),
|
||||
@@ -1087,6 +1098,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.targetAccountOverride) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedDatabase = useDatabases
|
||||
.getState()
|
||||
.databases?.find((database) => database.id() === this.state.selectedDatabaseId);
|
||||
@@ -1124,7 +1139,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
// }
|
||||
|
||||
private shouldShowCollectionThroughputInput(): boolean {
|
||||
if (isServerlessAccount()) {
|
||||
if (isServerlessAccount(this.props.targetAccountOverride)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1140,7 +1155,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
||||
if (!isFreeTierAccount()) {
|
||||
if (!isFreeTierAccount(this.props.targetAccountOverride)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1148,7 +1163,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private shouldShowVectorSearchParameters() {
|
||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||
const targetAccount = this.props.targetAccountOverride;
|
||||
return (
|
||||
isVectorSearchEnabled(targetAccount) &&
|
||||
(isServerlessAccount(targetAccount) || this.shouldShowCollectionThroughputInput())
|
||||
);
|
||||
}
|
||||
|
||||
private shouldShowFullTextSearchParameters() {
|
||||
@@ -1227,7 +1246,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private getAnalyticalStorageTtl(): number {
|
||||
if (!isSynapseLinkEnabled()) {
|
||||
if (!isSynapseLinkEnabled(this.props.targetAccountOverride)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1367,13 +1386,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
||||
vectorEmbeddingPolicy,
|
||||
fullTextPolicy: this.state.fullTextPolicy,
|
||||
targetAccountOverride: this.props.targetAccountOverride,
|
||||
};
|
||||
|
||||
this.setState({ isExecuting: true });
|
||||
|
||||
try {
|
||||
await createCollection(createCollectionParams);
|
||||
await this.props.explorer.refreshAllDatabases();
|
||||
if (!this.props.isCopyJobFlow) {
|
||||
await this.props.explorer.refreshAllDatabases();
|
||||
}
|
||||
if (this.props.isQuickstart) {
|
||||
const database = useDatabases.getState().findDatabaseWithId(databaseId);
|
||||
if (database) {
|
||||
@@ -1402,7 +1424,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
}
|
||||
} 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 });
|
||||
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
||||
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 { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { AccountOverride } from "Contracts/DataModels";
|
||||
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { Keys, t } from "Localization";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
@@ -68,7 +69,10 @@ export function getPartitionKey(isQuickstart?: boolean): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function isFreeTierAccount(): boolean {
|
||||
export function isFreeTierAccount(targetAccountOverride?: AccountOverride): boolean {
|
||||
if (targetAccountOverride) {
|
||||
return targetAccountOverride.enableFreeTier ?? false;
|
||||
}
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
"browse": "Browse",
|
||||
"increaseValueBy1": "Increase value by 1",
|
||||
"decreaseValueBy1": "Decrease value by 1",
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"preview": "Preview"
|
||||
},
|
||||
"splashScreen": {
|
||||
@@ -992,5 +994,164 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import { AccountOverride } from "Contracts/DataModels";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export const isCapabilityEnabled = (capabilityName: string): boolean => {
|
||||
export const isCapabilityEnabled = (capabilityName: string, targetAccountOverride?: AccountOverride): boolean => {
|
||||
const { databaseAccount } = userContext;
|
||||
if (databaseAccount && databaseAccount.properties && databaseAccount.properties.capabilities) {
|
||||
return databaseAccount.properties.capabilities.some((capability) => capability.name === capabilityName);
|
||||
const capabilities = targetAccountOverride?.capabilities || databaseAccount?.properties?.capabilities;
|
||||
if (capabilities) {
|
||||
return capabilities.some((capability) => capability.name === capabilityName);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isServerlessAccount = (): boolean => {
|
||||
export const isServerlessAccount = (targetAccountOverride?: AccountOverride): boolean => {
|
||||
const { databaseAccount } = userContext;
|
||||
const capacityMode = targetAccountOverride?.capacityMode || databaseAccount?.properties?.capacityMode;
|
||||
return (
|
||||
databaseAccount?.properties?.capacityMode === Constants.CapacityMode.Serverless ||
|
||||
isCapabilityEnabled(Constants.CapabilityNames.EnableServerless)
|
||||
capacityMode === Constants.CapacityMode.Serverless ||
|
||||
isCapabilityEnabled(Constants.CapabilityNames.EnableServerless, targetAccountOverride)
|
||||
);
|
||||
};
|
||||
|
||||
export const isVectorSearchEnabled = (): boolean => {
|
||||
export const isVectorSearchEnabled = (targetAccountOverride?: AccountOverride): boolean => {
|
||||
return (
|
||||
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 (
|
||||
userContext.apiType === "SQL" &&
|
||||
isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearchPreviewFeatures)
|
||||
isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearchPreviewFeatures, targetAccountOverride)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ export const assignRole = async (
|
||||
return null;
|
||||
}
|
||||
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 path = `${accountScope}/sqlRoleAssignments/${roleAssignmentName}`;
|
||||
|
||||
|
||||
@@ -141,6 +141,9 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes
|
||||
params.set("nosqlRbacToken", nosqlRbacToken);
|
||||
params.set("enableaaddataplane", "true");
|
||||
}
|
||||
if (enablecontainercopy) {
|
||||
params.set("enablecontainercopy", "true");
|
||||
}
|
||||
break;
|
||||
|
||||
case TestAccount.SQLContainerCopyOnly:
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe("Container Copy - Offline Migration", () => {
|
||||
let panel: Locator;
|
||||
let frame: Frame;
|
||||
let expectedJobName: string;
|
||||
let targetAccountName: string;
|
||||
let sourceAccountName: string;
|
||||
let expectedSubscriptionName: string;
|
||||
let expectedCopyJobNameInitial: string;
|
||||
|
||||
@@ -28,7 +28,7 @@ test.describe("Container Copy - Offline Migration", () => {
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
expectedJobName = `offline_test_job_${Date.now()}`;
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
sourceAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
});
|
||||
|
||||
test.afterEach("Cleanup after offline migration test", async () => {
|
||||
@@ -53,7 +53,7 @@ test.describe("Container Copy - Offline Migration", () => {
|
||||
|
||||
// Setup subscription and account
|
||||
const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
|
||||
const expectedAccountName = targetAccountName;
|
||||
const expectedAccountName = sourceAccountName;
|
||||
expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText();
|
||||
|
||||
await subscriptionDropdown.click();
|
||||
@@ -185,8 +185,8 @@ test.describe("Container Copy - Offline Migration", () => {
|
||||
// Verify job preview details
|
||||
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
|
||||
await expect(previewContainer).toBeVisible();
|
||||
await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(expectedSubscriptionName);
|
||||
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedAccountName);
|
||||
await expect(previewContainer.getByTestId("destination-subscription-name")).toHaveText(expectedSubscriptionName);
|
||||
await expect(previewContainer.getByTestId("destination-account-name")).toHaveText(expectedAccountName);
|
||||
|
||||
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
|
||||
await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial));
|
||||
|
||||
@@ -15,14 +15,14 @@ test.describe("Container Copy - Online Migration", () => {
|
||||
let wrapper: Locator;
|
||||
let panel: Locator;
|
||||
let frame: Frame;
|
||||
let targetAccountName: string;
|
||||
let sourceAccountName: string;
|
||||
|
||||
test.beforeEach("Setup for online migration test", async ({ browser }) => {
|
||||
contexts = await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
|
||||
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
sourceAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
});
|
||||
|
||||
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
|
||||
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 onlineMigrationJobName = await jobNameInput.inputValue();
|
||||
@@ -112,7 +112,7 @@ test.describe("Container Copy - Online Migration", () => {
|
||||
|
||||
const copyJobCreationPromise = waitForApiResponse(
|
||||
page,
|
||||
`${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}`,
|
||||
`${sourceAccountName}/dataTransferJobs/${onlineMigrationJobName}`,
|
||||
"PUT",
|
||||
);
|
||||
await copyButton.click();
|
||||
@@ -149,7 +149,7 @@ test.describe("Container Copy - Online Migration", () => {
|
||||
|
||||
const pauseResponse = await waitForApiResponse(
|
||||
page,
|
||||
`${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}/pause`,
|
||||
`${sourceAccountName}/dataTransferJobs/${onlineMigrationJobName}/pause`,
|
||||
"POST",
|
||||
);
|
||||
expect(pauseResponse.ok()).toBe(true);
|
||||
|
||||
@@ -10,13 +10,14 @@ test.describe("Container Copy - Permission Screen Verification", () => {
|
||||
let wrapper: Locator;
|
||||
let panel: Locator;
|
||||
let frame: Frame;
|
||||
let sourceAccountName: string;
|
||||
let targetAccountName: string;
|
||||
let expectedSourceAccountName: string;
|
||||
|
||||
test.beforeEach("Setup for each test", async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQL));
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
sourceAccountName = getAccountName(TestAccount.SQL);
|
||||
});
|
||||
|
||||
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 filteredItems = [];
|
||||
for (const item of allDropdownItems) {
|
||||
const testContent = (await item.textContent()) ?? "";
|
||||
if (testContent.trim() !== targetAccountName.trim()) {
|
||||
filteredItems.push(item);
|
||||
if (testContent.trim() === targetAccountName.trim()) {
|
||||
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
|
||||
const migrationTypeContainer = panel.getByTestId("migration-type");
|
||||
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();
|
||||
|
||||
// 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 = {
|
||||
identity: {
|
||||
type: "SystemAssigned",
|
||||
@@ -169,7 +162,7 @@ test.describe("Container Copy - Permission Screen Verification", () => {
|
||||
// Verify new page opens with correct URL pattern
|
||||
page.context().on("page", async (newPage) => {
|
||||
const expectedUrlEndPattern = new RegExp(
|
||||
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${expectedSourceAccountName}/backupRestore`,
|
||||
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${sourceAccountName}/backupRestore`,
|
||||
);
|
||||
expect(newPage.url()).toMatch(expectedUrlEndPattern);
|
||||
await newPage.close();
|
||||
@@ -188,8 +181,10 @@ test.describe("Container Copy - Permission Screen Verification", () => {
|
||||
await expect(pitrBtn).not.toBeVisible();
|
||||
|
||||
// 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(
|
||||
`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/sqlRoleAssignments*`,
|
||||
`**/Microsoft.DocumentDB/databaseAccounts/${sourceAccountName}/sqlRoleAssignments*`,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -198,7 +193,7 @@ test.describe("Container Copy - Permission Screen Verification", () => {
|
||||
value: [
|
||||
{
|
||||
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({
|
||||
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 = {
|
||||
identity: {
|
||||
type: "SystemAssigned",
|
||||
|
||||
Reference in New Issue
Block a user