mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-03-28 02:53:31 +00:00
Compare commits
3 Commits
master
...
user/bchou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
129c25c544 | ||
|
|
1eba0c4ea2 | ||
|
|
15ad4242d5 |
@@ -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,
|
||||
|
||||
134
src/Common/dataAccess/createDatabase.test.ts
Normal file
134
src/Common/dataAccess/createDatabase.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
// 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",
|
||||
},
|
||||
};
|
||||
|
||||
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, readDatabasesForAccount } from "./readDatabases";
|
||||
|
||||
describe("readDatabases", () => {
|
||||
beforeAll(() => {
|
||||
@@ -42,3 +43,64 @@ describe("readDatabases", () => {
|
||||
expect(client).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readDatabasesForAccount", () => {
|
||||
const mockDatabase = { id: "testDb", _rid: "", _self: "", _etag: "", _ts: 0 };
|
||||
const mockArmResponse = { value: [{ properties: { resource: mockDatabase } }] };
|
||||
|
||||
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 readDatabasesForAccount("test-sub", "test-rg", "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 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 readDatabasesForAccount("sub", "rg", "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 readDatabasesForAccount("sub", "rg", "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 readDatabasesForAccount("sub", "rg", "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(readDatabasesForAccount("sub", "rg", "account")).rejects.toThrow("ARM request failed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,3 +112,20 @@ async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
|
||||
|
||||
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database);
|
||||
}
|
||||
|
||||
export async function readDatabasesForAccount(
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
): Promise<DataModels.Database[]> {
|
||||
const clearMessage = logConsoleProgress(`Querying databases for account ${accountName}`);
|
||||
try {
|
||||
const rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
|
||||
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? [];
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDatabasesForAccount", `Error while querying databases for account ${accountName}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,11 +404,18 @@ export interface AutoPilotOfferSettings {
|
||||
targetMaxThroughput?: number;
|
||||
}
|
||||
|
||||
export interface AccountOverride {
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
export interface CreateDatabaseParams {
|
||||
autoPilotMaxThroughput?: number;
|
||||
databaseId: string;
|
||||
databaseLevelThroughput?: boolean;
|
||||
offerThroughput?: number;
|
||||
targetAccountOverride?: AccountOverride;
|
||||
}
|
||||
|
||||
export interface CreateCollectionParamsBase {
|
||||
@@ -428,6 +435,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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -20,11 +20,11 @@ export default {
|
||||
createCopyJobPanelTitle: "Create copy job",
|
||||
|
||||
// Select Account Screen
|
||||
selectAccountDescription: "Please select a source account from which to copy.",
|
||||
selectAccountDescription: "Please select a destination account to copy to.",
|
||||
subscriptionDropdownLabel: "Subscription",
|
||||
subscriptionDropdownPlaceholder: "Select a subscription",
|
||||
sourceAccountDropdownLabel: "Account",
|
||||
sourceAccountDropdownPlaceholder: "Select an account",
|
||||
destinationAccountDropdownLabel: "Account",
|
||||
destinationAccountDropdownPlaceholder: "Select an account",
|
||||
migrationTypeOptions: {
|
||||
offline: {
|
||||
title: "Offline mode",
|
||||
@@ -47,14 +47,17 @@ export default {
|
||||
databaseDropdownPlaceholder: "Select a database",
|
||||
containerDropdownLabel: "Container",
|
||||
containerDropdownPlaceholder: "Select a container",
|
||||
createNewContainerSubHeading: "Select the properties for your container.",
|
||||
createNewContainerSubHeading: (accountName?: string) =>
|
||||
accountName
|
||||
? `Configure the properties for the new container on destination account "${accountName}".`
|
||||
: "Configure the properties for the new container.",
|
||||
createContainerButtonLabel: "Create a new container",
|
||||
createContainerHeading: "Create new container",
|
||||
|
||||
// Preview and Create Screen
|
||||
jobNameLabel: "Job name",
|
||||
sourceSubscriptionLabel: "Source subscription",
|
||||
sourceAccountLabel: "Source account",
|
||||
destinationSubscriptionLabel: "Destination subscription",
|
||||
destinationAccountLabel: "Destination account",
|
||||
sourceDatabaseLabel: "Source database",
|
||||
sourceContainerLabel: "Source container",
|
||||
targetDatabaseLabel: "Destination database",
|
||||
@@ -63,7 +66,7 @@ export default {
|
||||
// Assign Permissions Screen
|
||||
assignPermissions: {
|
||||
crossAccountDescription:
|
||||
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.",
|
||||
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.",
|
||||
intraAccountOnlineDescription: (accountName: string) =>
|
||||
`Follow the steps below to enable online copy on your "${accountName}" account.`,
|
||||
crossAccountConfiguration: {
|
||||
@@ -116,18 +119,18 @@ export default {
|
||||
popoverDescription: (accountName: string) =>
|
||||
`Assign the system-assigned managed identity as the default for "${accountName}". To confirm, click the "Yes" button. `,
|
||||
},
|
||||
readPermissionAssigned: {
|
||||
title: "Read permissions assigned to the default identity.",
|
||||
readWritePermissionAssigned: {
|
||||
title: "Read-write 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.",
|
||||
"To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.",
|
||||
tooltip: {
|
||||
content: "Learn more about",
|
||||
hrefText: "Read permissions.",
|
||||
hrefText: "Read-write permissions.",
|
||||
href: "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
|
||||
},
|
||||
popoverTitle: "Read permissions assigned to default identity.",
|
||||
popoverTitle: "Assign read-write permissions to default identity.",
|
||||
popoverDescription:
|
||||
"Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button.",
|
||||
'Assign read-write permissions on the source account to the default identity of the destination account. To confirm, click the "Yes" button.',
|
||||
},
|
||||
pointInTimeRestore: {
|
||||
title: "Point In Time Restore enabled",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => {
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
};
|
||||
|
||||
const mockContextValue = {
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
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",
|
||||
@@ -101,7 +101,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
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 +119,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: jest.fn(),
|
||||
setContextError: jest.fn(),
|
||||
@@ -133,7 +133,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
const renderComponent = (contextValue = mockContextValue) => {
|
||||
return render(
|
||||
<CopyJobContext.Provider value={contextValue}>
|
||||
<AddReadPermissionToDefaultIdentity />
|
||||
<AddReadWritePermissionToDefaultIdentity />
|
||||
</CopyJobContext.Provider>,
|
||||
);
|
||||
};
|
||||
@@ -164,12 +164,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 +180,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
describe("Component Structure", () => {
|
||||
it("should display the description text", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument();
|
||||
expect(screen.getByText(ContainerCopyMessages.readWritePermissionAssigned.description)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the info tooltip", () => {
|
||||
@@ -212,10 +212,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
expect(screen.getByTestId("popover-message")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("popover-title")).toHaveTextContent(
|
||||
ContainerCopyMessages.readPermissionAssigned.popoverTitle,
|
||||
ContainerCopyMessages.readWritePermissionAssigned.popoverTitle,
|
||||
);
|
||||
expect(screen.getByTestId("popover-content")).toHaveTextContent(
|
||||
ContainerCopyMessages.readPermissionAssigned.popoverDescription,
|
||||
ContainerCopyMessages.readWritePermissionAssigned.popoverDescription,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -243,7 +243,7 @@ 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",
|
||||
@@ -264,7 +264,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleAddReadPermission Function", () => {
|
||||
describe("handleAddReadWritePermission Function", () => {
|
||||
beforeEach(() => {
|
||||
mockUseToggle.mockReturnValue([true, jest.fn()]);
|
||||
});
|
||||
@@ -312,7 +312,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
await waitFor(() => {
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
"Permission denied",
|
||||
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
|
||||
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -336,14 +336,14 @@ 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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -496,7 +496,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
|
||||
expect(updatedState).toEqual({
|
||||
...mockContextValue.copyJobState,
|
||||
sourceReadAccessFromTarget: true,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,27 +12,29 @@ import useToggle from "./hooks/useToggle";
|
||||
|
||||
const TooltipContent = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
||||
{ContainerCopyMessages.readWritePermissionAssigned.tooltip.content}
|
||||
<Link
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
|
||||
href={ContainerCopyMessages.readWritePermissionAssigned.tooltip.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
||||
{ContainerCopyMessages.readWritePermissionAssigned.tooltip.hrefText}
|
||||
</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;
|
||||
|
||||
try {
|
||||
const {
|
||||
subscriptionId: sourceSubscriptionId,
|
||||
@@ -47,16 +49,17 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
||||
sourceAccountName,
|
||||
target?.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,12 +69,12 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
||||
return (
|
||||
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
<Text className="toggle-label">
|
||||
{ContainerCopyMessages.readPermissionAssigned.description} 
|
||||
{ContainerCopyMessages.readWritePermissionAssigned.description} 
|
||||
<InfoTooltip content={TooltipContent} />
|
||||
</Text>
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={readPermissionAssigned}
|
||||
checked={readWritePermissionAssigned}
|
||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||
onChange={onToggle}
|
||||
@@ -83,15 +86,15 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
|
||||
/>
|
||||
<PopoverMessage
|
||||
isLoading={loading}
|
||||
visible={readPermissionAssigned}
|
||||
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
|
||||
visible={readWritePermissionAssigned}
|
||||
title={ContainerCopyMessages.readWritePermissionAssigned.popoverTitle}
|
||||
onCancel={() => onToggle(null, false)}
|
||||
onPrimary={handleAddReadPermission}
|
||||
onPrimary={handleAddReadWritePermission}
|
||||
>
|
||||
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
|
||||
{ContainerCopyMessages.readWritePermissionAssigned.popoverDescription}
|
||||
</PopoverMessage>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddReadPermissionToDefaultIdentity;
|
||||
export default AddReadWritePermissionToDefaultIdentity;
|
||||
@@ -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",
|
||||
@@ -201,7 +201,7 @@ describe("AssignPermissions Component", () => {
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
id: "readPermissionAssigned",
|
||||
id: "readWritePermissionAssigned",
|
||||
title: "Read Permission Assigned",
|
||||
Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>,
|
||||
disabled: false,
|
||||
@@ -347,7 +347,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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 source account to the default identity of the destination 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 source account to the default identity of the destination 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 source account to the default identity of the destination 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 source account to the default identity of the destination 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 source account to the default identity of the destination 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 source account to the default identity of the destination 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 source account to the default identity of the destination 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>
|
||||
|
||||
@@ -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 destination account has read-write access to the source 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 destination account has read-write access to the source 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 destination account has read-write access to the source 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 destination account has read-write access to the source 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 destination account has read-write access to the source 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 destination account has read-write access to the source account by completing the following steps.
|
||||
</span>
|
||||
<div
|
||||
data-testid="shimmer-tree"
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
@@ -299,7 +299,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -337,7 +337,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -358,16 +358,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",
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -398,7 +399,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -407,7 +408,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 +438,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -476,7 +479,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -546,7 +549,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -568,12 +571,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 +586,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 +612,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
@@ -630,12 +634,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 +657,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 +679,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
},
|
||||
{
|
||||
id: "role-2",
|
||||
name: "00000000-0000-0000-0000-000000000001",
|
||||
name: "00000000-0000-0000-0000-000000000002",
|
||||
permissions: [],
|
||||
assignableScopes: [],
|
||||
resourceGroup: "",
|
||||
@@ -685,7 +689,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
@@ -66,9 +68,9 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: SECTION_IDS.readPermissionAssigned,
|
||||
title: ContainerCopyMessages.readPermissionAssigned.title,
|
||||
Component: AddReadPermissionToDefaultIdentity,
|
||||
id: SECTION_IDS.readWritePermissionAssigned,
|
||||
title: ContainerCopyMessages.readWritePermissionAssigned.title,
|
||||
Component: AddReadWritePermissionToDefaultIdentity,
|
||||
disabled: true,
|
||||
validate: async (state: CopyJobContextState) => {
|
||||
const principalId = state?.target?.account?.identity?.principalId;
|
||||
@@ -87,7 +89,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
||||
);
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: null,
|
||||
@@ -109,7 +109,7 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument();
|
||||
expect(container.querySelector(".addCollectionPanelHeader")).toBeInTheDocument();
|
||||
expect(container.querySelector(".addCollectionPanelBody")).toBeInTheDocument();
|
||||
expect(screen.getByText(ContainerCopyMessages.createNewContainerSubHeading)).toBeInTheDocument();
|
||||
expect(screen.getByText(ContainerCopyMessages.createNewContainerSubHeading())).toBeInTheDocument();
|
||||
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import { IDropdownOption, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||
import { readDatabasesForAccount } from "Common/dataAccess/readDatabases";
|
||||
import { AccountOverride } from "Contracts/DataModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { produce } from "immer";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AddCollectionPanel } from "../../../../Panes/AddCollectionPanel/AddCollectionPanel";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
|
||||
type AddCollectionPanelWrapperProps = {
|
||||
explorer?: Explorer;
|
||||
@@ -13,7 +16,26 @@ 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,
|
||||
};
|
||||
}, [copyJobState?.target?.account?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const sidePanelStore = useSidePanel.getState();
|
||||
@@ -25,6 +47,52 @@ 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 readDatabasesForAccount(
|
||||
targetAccountOverride.subscriptionId,
|
||||
targetAccountOverride.resourceGroup,
|
||||
targetAccountOverride.accountName,
|
||||
);
|
||||
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 +106,37 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
||||
[goBack],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack horizontalAlign="center" verticalAlign="center" styles={{ root: { padding: 20 } }}>
|
||||
<Spinner size={SpinnerSize.large} label="Loading destination account databases..." />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (permissionError) {
|
||||
return (
|
||||
<Stack styles={{ root: { padding: 20 } }}>
|
||||
<MessageBar messageBarType={MessageBarType.error}>{permissionError}</MessageBar>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack className="addCollectionPanelWrapper">
|
||||
<Stack.Item className="addCollectionPanelHeader">
|
||||
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
||||
<Text className="themeText">
|
||||
{ContainerCopyMessages.createNewContainerSubHeading(targetAccountOverride?.accountName)}
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -87,18 +87,18 @@ describe("PreviewCopyJob", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "target-database",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -146,7 +146,7 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with missing source subscription information", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
@@ -165,7 +165,7 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with missing source account information", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: null,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
@@ -184,13 +184,13 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with undefined database and container names", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
@@ -219,7 +219,7 @@ describe("PreviewCopyJob", () => {
|
||||
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: longNameSubscription,
|
||||
subscriptionId: longNameSubscription.subscriptionId,
|
||||
account: longNameAccount,
|
||||
databaseId: "long-database-name-for-testing-purposes",
|
||||
containerId: "long-container-name-for-testing-purposes",
|
||||
@@ -253,13 +253,13 @@ describe("PreviewCopyJob", () => {
|
||||
it("should handle special characters in database and container names", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "test-db_with@special#chars",
|
||||
containerId: "test-container_with@special#chars",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "target-db_with@special#chars",
|
||||
containerId: "target-container_with@special#chars",
|
||||
@@ -285,12 +285,12 @@ describe("PreviewCopyJob", () => {
|
||||
|
||||
const mockContext = createMockContext({
|
||||
target: {
|
||||
subscriptionId: "target-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: targetAccount,
|
||||
databaseId: "target-database",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadAccessFromTarget: true,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
@@ -360,7 +360,7 @@ describe("PreviewCopyJob", () => {
|
||||
);
|
||||
|
||||
expect(getByText(/Job name/i)).toBeInTheDocument();
|
||||
expect(getByText(/Source subscription/i)).toBeInTheDocument();
|
||||
expect(getByText(/Source account/i)).toBeInTheDocument();
|
||||
expect(getByText(/Destination subscription/i)).toBeInTheDocument();
|
||||
expect(getByText(/Destination account/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,15 +36,15 @@ const PreviewCopyJob: React.FC = () => {
|
||||
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
||||
</FieldRow>
|
||||
<Stack>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||
<Text data-test="source-subscription-name" className="themeText">
|
||||
{copyJobState.source?.subscription?.displayName}
|
||||
<Text className="bold themeText">{ContainerCopyMessages.destinationSubscriptionLabel}</Text>
|
||||
<Text data-test="destination-subscription-name" className="themeText">
|
||||
{copyJobState.target?.subscription?.displayName}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text data-test="source-account-name" className="themeText">
|
||||
{copyJobState.source?.account?.name}
|
||||
<Text className="bold themeText">{ContainerCopyMessages.destinationAccountLabel}</Text>
|
||||
<Text data-test="destination-account-name" className="themeText">
|
||||
{copyJobState.target?.account?.name}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -38,6 +38,12 @@ describe("AccountDropdown", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
displayName: "Test Subscription",
|
||||
@@ -46,13 +52,7 @@ describe("AccountDropdown", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
} as CopyJobContextState;
|
||||
|
||||
const mockCopyJobContextValue = {
|
||||
@@ -129,11 +129,11 @@ describe("AccountDropdown", () => {
|
||||
renderWithContext();
|
||||
|
||||
expect(
|
||||
screen.getByText(`${ContainerCopyMessages.sourceAccountDropdownLabel}:`, { exact: true }),
|
||||
screen.getByText(`${ContainerCopyMessages.destinationAccountDropdownLabel}:`, { exact: true }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox")).toHaveAttribute(
|
||||
"aria-label",
|
||||
ContainerCopyMessages.sourceAccountDropdownLabel,
|
||||
ContainerCopyMessages.destinationAccountDropdownLabel,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -202,7 +202,7 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const newState = stateUpdateFunction(mockCopyJobState);
|
||||
expect(newState.source.account).toEqual({
|
||||
expect(newState.target.account).toEqual({
|
||||
...mockDatabaseAccount1,
|
||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||
});
|
||||
@@ -226,20 +226,21 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const newState = stateUpdateFunction(mockCopyJobState);
|
||||
expect(newState.source.account).toEqual({
|
||||
expect(newState.target.account).toEqual({
|
||||
...mockDatabaseAccount2,
|
||||
id: normalizeAccountId(mockDatabaseAccount2.id),
|
||||
});
|
||||
});
|
||||
|
||||
it("should keep current account if it exists in the filtered list", async () => {
|
||||
const normalizedAccount1 = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
|
||||
const contextWithSelectedAccount = {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
account: mockDatabaseAccount1,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: normalizedAccount1,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -256,12 +257,9 @@ describe("AccountDropdown", () => {
|
||||
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
||||
expect(newState).toEqual({
|
||||
...contextWithSelectedAccount.copyJobState,
|
||||
source: {
|
||||
...contextWithSelectedAccount.copyJobState.source,
|
||||
account: {
|
||||
...mockDatabaseAccount1,
|
||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||
},
|
||||
target: {
|
||||
...contextWithSelectedAccount.copyJobState.target,
|
||||
account: normalizedAccount1,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -297,8 +295,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: portalAccount,
|
||||
},
|
||||
},
|
||||
@@ -323,8 +321,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: hostedAccount,
|
||||
},
|
||||
},
|
||||
@@ -361,8 +359,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
subscription: null,
|
||||
},
|
||||
} as CopyJobContextState,
|
||||
@@ -376,13 +374,13 @@ describe("AccountDropdown", () => {
|
||||
});
|
||||
|
||||
it("should not update state if account is already selected and the same", async () => {
|
||||
const selectedAccount = mockDatabaseAccount1;
|
||||
const selectedAccount = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
|
||||
const contextWithSelectedAccount = {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: selectedAccount,
|
||||
},
|
||||
},
|
||||
@@ -409,7 +407,7 @@ describe("AccountDropdown", () => {
|
||||
renderWithContext();
|
||||
|
||||
const dropdown = screen.getByRole("combobox");
|
||||
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.sourceAccountDropdownLabel);
|
||||
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.destinationAccountDropdownLabel);
|
||||
});
|
||||
|
||||
it("should have required attribute", () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const normalizeAccountId = (id: string = "") => {
|
||||
export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||
|
||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||
const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
||||
const sqlApiOnlyAccounts = (allAccounts || [])
|
||||
.filter((account) => apiType(account) === "SQL")
|
||||
@@ -36,11 +36,11 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
|
||||
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
||||
setCopyJobState((prevState) => {
|
||||
if (prevState.source?.account?.id !== newAccount.id) {
|
||||
if (prevState.target?.account?.id !== newAccount.id) {
|
||||
return {
|
||||
...prevState,
|
||||
source: {
|
||||
...prevState.source,
|
||||
target: {
|
||||
...prevState.target,
|
||||
account: newAccount,
|
||||
},
|
||||
};
|
||||
@@ -51,13 +51,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
||||
const currentAccountId = copyJobState?.source?.account?.id;
|
||||
const currentAccountId = copyJobState?.target?.account?.id;
|
||||
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
|
||||
const selectedAccountId = currentAccountId || predefinedAccountId;
|
||||
|
||||
const targetAccount: DatabaseAccount | null =
|
||||
const matchedAccount: DatabaseAccount | null =
|
||||
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
|
||||
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
|
||||
updateCopyJobState(matchedAccount || sqlApiOnlyAccounts[0]);
|
||||
}
|
||||
}, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]);
|
||||
|
||||
@@ -77,13 +77,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
};
|
||||
|
||||
const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0;
|
||||
const selectedAccountId = normalizeAccountId(copyJobState?.source?.account?.id ?? "");
|
||||
const selectedAccountId = normalizeAccountId(copyJobState?.target?.account?.id ?? "");
|
||||
|
||||
return (
|
||||
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}>
|
||||
<FieldRow label={ContainerCopyMessages.destinationAccountDropdownLabel}>
|
||||
<Dropdown
|
||||
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder}
|
||||
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel}
|
||||
placeholder={ContainerCopyMessages.destinationAccountDropdownPlaceholder}
|
||||
ariaLabel={ContainerCopyMessages.destinationAccountDropdownLabel}
|
||||
options={accountOptions}
|
||||
disabled={isAccountDropdownDisabled}
|
||||
required
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("MigrationType", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: { currentScreen: "selectAccount" },
|
||||
|
||||
@@ -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={ContainerCopyMessages.subscriptionDropdownLabel}>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: { subscriptionId: "test-subscription-id" },
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
name: "test-account",
|
||||
@@ -82,7 +82,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
containerId: "container1",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: { subscriptionId: "test-subscription-id" },
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
name: "test-account",
|
||||
@@ -90,7 +90,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
databaseId: "db2",
|
||||
containerId: "container2",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
};
|
||||
|
||||
const mockMemoizedData = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
||||
sourceContainerLabel: "Source Container",
|
||||
targetDatabaseLabel: "Destination Database",
|
||||
targetContainerLabel: "Destination Container",
|
||||
sourceAccountLabel: "Source Account",
|
||||
destinationAccountLabel: "Destination account",
|
||||
MonitorJobs: {
|
||||
Columns: {
|
||||
lastUpdatedTime: "Date & time",
|
||||
@@ -102,8 +102,8 @@ describe("CopyJobDetails", () => {
|
||||
expect(screen.getByText("Date & time")).toBeInTheDocument();
|
||||
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Source Account")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceAccount")).toBeInTheDocument();
|
||||
expect(screen.getByText("Destination account")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetAccount")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Mode")).toBeInTheDocument();
|
||||
expect(screen.getByText("Offline")).toBeInTheDocument();
|
||||
@@ -263,7 +263,7 @@ describe("CopyJobDetails", () => {
|
||||
expect(screen.getByText("complex_source_container_with_underscores")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex_target_container_with_underscores")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex.source.account")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex.target.account")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -322,11 +322,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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.destinationAccountLabel}</Text>
|
||||
<Text className="themeText">{job.Destination?.remoteAccountName}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={sectionCss.verticalAlign}>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
||||
|
||||
@@ -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;
|
||||
|
||||
168
src/Explorer/Explorer.test.tsx
Normal file
168
src/Explorer/Explorer.test.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
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 { 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: [],
|
||||
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",
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -227,7 +227,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",
|
||||
@@ -249,7 +253,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,
|
||||
},
|
||||
@@ -258,7 +262,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)}`);
|
||||
|
||||
@@ -12,4 +12,56 @@ 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",
|
||||
};
|
||||
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",
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
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 {
|
||||
@@ -68,6 +69,8 @@ export interface AddCollectionPanelProps {
|
||||
isQuickstart?: boolean;
|
||||
isCopyJobFlow?: boolean;
|
||||
onSubmitSuccess?: (collectionData: { databaseId: string; collectionId: string }) => void;
|
||||
targetAccountOverride?: AccountOverride;
|
||||
externalDatabaseOptions?: IDropdownOption[];
|
||||
}
|
||||
|
||||
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||
@@ -876,7 +879,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 } }}
|
||||
/>
|
||||
@@ -1061,6 +1064,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(),
|
||||
@@ -1148,6 +1154,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);
|
||||
@@ -1438,13 +1448,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) {
|
||||
@@ -1473,7 +1486,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);
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -118,6 +118,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 () => {
|
||||
@@ -50,22 +51,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 });
|
||||
@@ -81,7 +74,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",
|
||||
@@ -139,7 +132,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();
|
||||
@@ -158,8 +151,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,
|
||||
@@ -168,7 +163,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`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -183,14 +178,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