mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-14 09:17:29 +01:00
Merge branch 'master' into users/chskelt/pkupdate
This commit is contained in:
Generated
+20
-17
@@ -875,9 +875,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz",
|
||||
"integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==",
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
||||
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -1752,15 +1753,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-systemjs": {
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz",
|
||||
"integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==",
|
||||
"version": "7.29.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
|
||||
"integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.25.0",
|
||||
"@babel/helper-plugin-utils": "^7.24.8",
|
||||
"@babel/helper-validator-identifier": "^7.24.7",
|
||||
"@babel/traverse": "^7.25.0"
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/traverse": "^7.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -10991,13 +10993,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
@@ -15560,9 +15562,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -15570,6 +15572,7 @@
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
|
||||
@@ -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 { Keys, t } from "Localization";
|
||||
import {
|
||||
convertTime,
|
||||
convertToCamelCase,
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+5
-6
@@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => {
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
};
|
||||
|
||||
const mockContextValue = {
|
||||
@@ -197,11 +197,10 @@ describe("AddManagedIdentity", () => {
|
||||
});
|
||||
|
||||
it("displays correct enablement description with account name", () => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
t(Keys.containerCopy.addManagedIdentity.enablementDescription, { accountName: "test-target-account" }),
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
const expectedDescription = t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
|
||||
accountName: mockCopyJobState.source.account.name,
|
||||
});
|
||||
expect(screen.getByText(expectedDescription)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls handleAddSystemIdentity when primary button clicked", async () => {
|
||||
|
||||
+10
-6
@@ -39,7 +39,13 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
|
||||
<InfoTooltip content={managedIdentityTooltip} />
|
||||
</Text>
|
||||
<Toggle data-test="btn-toggle" checked={systemAssigned} onText="On" offText="Off" onChange={onToggle} />
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={systemAssigned}
|
||||
onText={t(Keys.common.on)}
|
||||
offText={t(Keys.common.off)}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
<PopoverMessage
|
||||
isLoading={loading}
|
||||
visible={systemAssigned}
|
||||
@@ -47,11 +53,9 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
onCancel={() => onToggle(null, false)}
|
||||
onPrimary={handleAddSystemIdentity}
|
||||
>
|
||||
{copyJobState.target?.account?.name
|
||||
? t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
|
||||
accountName: copyJobState.target?.account?.name,
|
||||
})
|
||||
: ""}
|
||||
{t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
|
||||
accountName: copyJobState.source?.account?.name,
|
||||
})}
|
||||
</PopoverMessage>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
+55
-51
@@ -4,7 +4,7 @@ import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
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(t(Keys.containerCopy.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(
|
||||
t(Keys.containerCopy.readPermissionAssigned.popoverTitle),
|
||||
t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle),
|
||||
);
|
||||
expect(screen.getByTestId("popover-content")).toHaveTextContent(
|
||||
t(Keys.containerCopy.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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+31
-28
@@ -12,51 +12,54 @@ import useToggle from "./hooks/useToggle";
|
||||
|
||||
const TooltipContent = (
|
||||
<Text>
|
||||
{t(Keys.containerCopy.readPermissionAssigned.tooltipContent)}
|
||||
{t(Keys.containerCopy.readWritePermissionAssigned.tooltipContent)}
|
||||
<Link
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
href={t(Keys.containerCopy.readPermissionAssigned.tooltipHref)}
|
||||
href={t(Keys.containerCopy.readWritePermissionAssigned.tooltipHref)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t(Keys.containerCopy.readPermissionAssigned.tooltipHrefText)}
|
||||
{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">
|
||||
{t(Keys.containerCopy.readPermissionAssigned.description)} 
|
||||
{t(Keys.containerCopy.readWritePermissionAssigned.description)} 
|
||||
<InfoTooltip content={TooltipContent} />
|
||||
</Text>
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={readPermissionAssigned}
|
||||
onText="On"
|
||||
offText="Off"
|
||||
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={t(Keys.containerCopy.readPermissionAssigned.popoverTitle)}
|
||||
visible={readWritePermissionAssigned}
|
||||
title={t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle)}
|
||||
onCancel={() => onToggle(null, false)}
|
||||
onPrimary={handleAddReadPermission}
|
||||
onPrimary={handleAddReadWritePermission}
|
||||
>
|
||||
{t(Keys.containerCopy.readPermissionAssigned.popoverDescription)}
|
||||
{t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription)}
|
||||
</PopoverMessage>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddReadPermissionToDefaultIdentity;
|
||||
export default AddReadWritePermissionToDefaultIdentity;
|
||||
+12
-12
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
@@ -203,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,
|
||||
@@ -349,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",
|
||||
|
||||
+2
-2
@@ -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 { Keys, t } from "Localization";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { isIntraAccountCopy } from "../../../CopyJobUtils";
|
||||
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
|
||||
@@ -106,7 +106,7 @@ const AssignPermissions = () => {
|
||||
tokens={{ childrenGap: 20 }}
|
||||
>
|
||||
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
|
||||
{isSameAccount && copyJobState?.migrationType === CopyJobMigrationType.Online
|
||||
? t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, {
|
||||
accountName: copyJobState?.source?.account?.name || "",
|
||||
})
|
||||
|
||||
+21
-6
@@ -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",
|
||||
@@ -117,7 +123,7 @@ describe("DefaultManagedIdentity", () => {
|
||||
renderComponent();
|
||||
|
||||
const description = screen.getByText(
|
||||
t(Keys.containerCopy.defaultManagedIdentity.description, { accountName: "test-cosmos-account" }),
|
||||
/Set the system-assigned managed identity as default for "test-cosmos-account"/,
|
||||
);
|
||||
expect(description).toBeInTheDocument();
|
||||
});
|
||||
@@ -127,8 +133,8 @@ describe("DefaultManagedIdentity", () => {
|
||||
|
||||
const tooltip = screen.getByTestId("info-tooltip");
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(tooltip).toHaveTextContent(t(Keys.containerCopy.defaultManagedIdentity.tooltipContent));
|
||||
expect(tooltip).toHaveTextContent(t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText));
|
||||
expect(tooltip).toHaveTextContent("Learn more about");
|
||||
expect(tooltip).toHaveTextContent("Default Managed Identities.");
|
||||
});
|
||||
|
||||
it("should render the toggle button with correct initial state", () => {
|
||||
@@ -170,7 +176,7 @@ describe("DefaultManagedIdentity", () => {
|
||||
|
||||
const content = screen.getByTestId("popover-content");
|
||||
expect(content).toHaveTextContent(
|
||||
t(Keys.containerCopy.defaultManagedIdentity.popoverDescription, { accountName: "test-cosmos-account" }).trim(),
|
||||
/Assign the system-assigned managed identity as the default for "test-cosmos-account"/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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("On");
|
||||
const offText = screen.queryByText("Off");
|
||||
const onText = screen.queryByText(t(Keys.common.on));
|
||||
const offText = screen.queryByText(t(Keys.common.off));
|
||||
|
||||
expect(onText || offText).toBeTruthy();
|
||||
});
|
||||
|
||||
+4
-2
@@ -32,7 +32,9 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
return (
|
||||
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
<div className="toggle-label">
|
||||
{t(Keys.containerCopy.defaultManagedIdentity.description, { accountName: copyJobState?.target?.account?.name })}{" "}
|
||||
{t(Keys.containerCopy.defaultManagedIdentity.description, {
|
||||
accountName: copyJobState?.source?.account?.name,
|
||||
})}{" "}
|
||||
|
||||
<InfoTooltip content={managedIdentityTooltip} />
|
||||
</div>
|
||||
@@ -56,7 +58,7 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
|
||||
onPrimary={handleAddSystemIdentity}
|
||||
>
|
||||
{t(Keys.containerCopy.defaultManagedIdentity.popoverDescription, {
|
||||
accountName: copyJobState?.target?.account?.name,
|
||||
accountName: copyJobState?.source?.account?.name,
|
||||
})}
|
||||
</PopoverMessage>
|
||||
</Stack>
|
||||
|
||||
+3
-3
@@ -136,7 +136,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
renderComponent();
|
||||
|
||||
const refreshButton = screen.queryByRole("button", {
|
||||
name: "Refresh",
|
||||
name: t(Keys.common.refresh),
|
||||
});
|
||||
expect(refreshButton).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -322,7 +322,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
});
|
||||
|
||||
const refreshButton = screen.getByRole("button", {
|
||||
name: "Refresh",
|
||||
name: t(Keys.common.refresh),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -454,7 +454,7 @@ describe("OnlineCopyEnabled", () => {
|
||||
mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
const refreshButton = screen.getByRole("button", {
|
||||
name: "Refresh",
|
||||
name: t(Keys.common.refresh),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
|
||||
+3
-1
@@ -91,7 +91,9 @@ const OnlineCopyEnabled: React.FC = () => {
|
||||
});
|
||||
}
|
||||
setLoaderMessage(
|
||||
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { accountName: sourceAccountName }),
|
||||
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, {
|
||||
accountName: sourceAccountName,
|
||||
}),
|
||||
);
|
||||
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
|
||||
properties: {
|
||||
|
||||
+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();
|
||||
|
||||
+2
-2
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
+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);
|
||||
});
|
||||
});
|
||||
|
||||
+49
-31
@@ -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,11 +36,13 @@ 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,
|
||||
@@ -48,10 +50,10 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
||||
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
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -61,33 +63,33 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
||||
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: t(Keys.containerCopy.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 ?? []);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-2
@@ -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(t(Keys.containerCopy.selectContainers.createNewContainerSubHeading))).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(t(Keys.containerCopy.selectContainers.createNewContainerSubHeadingDefault)),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
+106
-5
@@ -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 { Keys, t } from "Localization";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AddCollectionPanel } from "../../../../Panes/AddCollectionPanel/AddCollectionPanel";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
|
||||
type AddCollectionPanelWrapperProps = {
|
||||
explorer?: Explorer;
|
||||
@@ -13,7 +16,30 @@ 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();
|
||||
@@ -25,6 +51,53 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
||||
};
|
||||
}, []);
|
||||
|
||||
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">{t(Keys.containerCopy.selectContainers.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
@@ -88,18 +88,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,
|
||||
};
|
||||
|
||||
@@ -147,7 +147,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",
|
||||
@@ -166,7 +166,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",
|
||||
@@ -185,13 +185,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: "",
|
||||
@@ -220,7 +220,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",
|
||||
@@ -254,13 +254,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",
|
||||
@@ -286,12 +286,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(
|
||||
@@ -360,8 +360,8 @@ describe("PreviewCopyJob", () => {
|
||||
</CopyJobContext.Provider>,
|
||||
);
|
||||
|
||||
expect(getByText(new RegExp(t(Keys.containerCopy.preview.jobNameLabel), "i"))).toBeInTheDocument();
|
||||
expect(getByText(new RegExp(t(Keys.containerCopy.preview.sourceSubscriptionLabel), "i"))).toBeInTheDocument();
|
||||
expect(getByText(new RegExp(t(Keys.containerCopy.preview.sourceAccountLabel), "i"))).toBeInTheDocument();
|
||||
expect(getByText(/Job name/i)).toBeInTheDocument();
|
||||
expect(getByText(/Destination subscription/i)).toBeInTheDocument();
|
||||
expect(getByText(/Destination account/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,15 +36,15 @@ const PreviewCopyJob: React.FC = () => {
|
||||
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
||||
</FieldRow>
|
||||
<Stack>
|
||||
<Text className="bold themeText">{t(Keys.containerCopy.preview.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">{t(Keys.containerCopy.preview.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>
|
||||
|
||||
+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>
|
||||
|
||||
+28
-30
@@ -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(`${t(Keys.containerCopy.selectAccount.sourceAccountDropdownLabel)}:`, { exact: true }),
|
||||
screen.getByText(`${t(Keys.containerCopy.selectAccount.accountDropdownLabel)}:`, { exact: true }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox")).toHaveAttribute(
|
||||
"aria-label",
|
||||
t(Keys.containerCopy.selectAccount.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", t(Keys.containerCopy.selectAccount.sourceAccountDropdownLabel));
|
||||
expect(dropdown).toHaveAttribute("aria-label", t(Keys.containerCopy.selectAccount.accountDropdownLabel));
|
||||
});
|
||||
|
||||
it("should have required attribute", () => {
|
||||
|
||||
+11
-11
@@ -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={t(Keys.containerCopy.selectAccount.sourceAccountDropdownLabel)}>
|
||||
<FieldRow label={t(Keys.containerCopy.selectAccount.accountDropdownLabel)}>
|
||||
<Dropdown
|
||||
placeholder={t(Keys.containerCopy.selectAccount.sourceAccountDropdownPlaceholder)}
|
||||
ariaLabel={t(Keys.containerCopy.selectAccount.sourceAccountDropdownLabel)}
|
||||
placeholder={t(Keys.containerCopy.selectAccount.accountDropdownPlaceholder)}
|
||||
ariaLabel={t(Keys.containerCopy.selectAccount.accountDropdownLabel)}
|
||||
options={accountOptions}
|
||||
disabled={isAccountDropdownDisabled}
|
||||
required
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ describe("MigrationType", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: { currentScreen: "selectAccount" },
|
||||
|
||||
+9
-4
@@ -47,8 +47,13 @@ export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
|
||||
};
|
||||
|
||||
const selectedKey = copyJobState?.migrationType ?? "";
|
||||
const selectedKeyLowercase = selectedKey.toLowerCase() as keyof typeof Keys.containerCopy.migrationType;
|
||||
const selectedKeyContent = Keys.containerCopy.migrationType[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={t(selectedKeyContent.description)} linkTarget="_blank" />
|
||||
<MarkdownRender source={t(migrationTypeDescriptionKey)} linkTarget="_blank" />
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
)}
|
||||
|
||||
+5
-5
@@ -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,7 +61,7 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
|
||||
}
|
||||
};
|
||||
|
||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||
const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||
|
||||
return (
|
||||
<FieldRow label={t(Keys.containerCopy.selectAccount.subscriptionDropdownLabel)}>
|
||||
|
||||
+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
-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);
|
||||
});
|
||||
|
||||
+3
-3
@@ -64,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",
|
||||
@@ -73,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",
|
||||
@@ -81,7 +81,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
databaseId: "db2",
|
||||
containerId: "container2",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
};
|
||||
|
||||
const mockMemoizedData = {
|
||||
|
||||
+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",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -280,7 +280,7 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"",
|
||||
null,
|
||||
"",
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
@@ -337,7 +337,7 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"",
|
||||
null,
|
||||
"",
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
@@ -381,7 +381,7 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"",
|
||||
null,
|
||||
"",
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
@@ -412,7 +412,7 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"",
|
||||
null,
|
||||
"",
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
@@ -828,7 +828,7 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"", // title
|
||||
null, // subText
|
||||
"", // 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 { Keys, t } from "Localization";
|
||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
|
||||
@@ -50,7 +50,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
"",
|
||||
null,
|
||||
"",
|
||||
t(Keys.common.confirm),
|
||||
() => handleClick(job, action, setUpdatingJobAction),
|
||||
t(Keys.common.cancel),
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("CopyJobColumns", () => {
|
||||
expect(actualKeys).toEqual(expectedKeys);
|
||||
});
|
||||
|
||||
it("should have correct column names from i18n", () => {
|
||||
it("should have correct column names", () => {
|
||||
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
|
||||
|
||||
expect(columns[0].name).toBe(t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IColumn } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
|
||||
|
||||
@@ -86,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();
|
||||
@@ -247,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -306,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 { Keys, t } from "Localization";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
|
||||
@@ -106,8 +106,8 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
||||
<Text className="themeText">{job.LastUpdatedTime}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={sectionCss.verticalAlign}>
|
||||
<Text className="bold themeText">{t(Keys.containerCopy.preview.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">{t(Keys.containerCopy.monitorJobs.columns.mode)}</Text>
|
||||
|
||||
@@ -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 { Keys, t } from "Localization";
|
||||
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
|
||||
const iconClass = mergeStyles({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { Keys, t } from "Localization";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1020,11 +1020,11 @@
|
||||
"panelTitle": "Create copy job"
|
||||
},
|
||||
"selectAccount": {
|
||||
"description": "Please select a source account from which to copy.",
|
||||
"description": "Please select a destination account to copy to.",
|
||||
"subscriptionDropdownLabel": "Subscription",
|
||||
"subscriptionDropdownPlaceholder": "Select a subscription",
|
||||
"sourceAccountDropdownLabel": "Account",
|
||||
"sourceAccountDropdownPlaceholder": "Select an account"
|
||||
"accountDropdownLabel": "Account",
|
||||
"accountDropdownPlaceholder": "Select an account"
|
||||
},
|
||||
"migrationType": {
|
||||
"offline": {
|
||||
@@ -1044,21 +1044,22 @@
|
||||
"databaseDropdownPlaceholder": "Select a database",
|
||||
"containerDropdownLabel": "Container",
|
||||
"containerDropdownPlaceholder": "Select a container",
|
||||
"createNewContainerSubHeading": "Select the properties for your 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",
|
||||
"sourceSubscriptionLabel": "Source subscription",
|
||||
"sourceAccountLabel": "Source account",
|
||||
"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 destination account has read access to the source account by completing the following steps.",
|
||||
"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",
|
||||
@@ -1094,14 +1095,14 @@
|
||||
"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. "
|
||||
},
|
||||
"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.",
|
||||
"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 permissions.",
|
||||
"tooltipHrefText": "Read-write permissions.",
|
||||
"tooltipHref": "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 \u201cYes\u201d button."
|
||||
"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",
|
||||
|
||||
@@ -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