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

This commit is contained in:
Bikram Choudhury
2026-03-26 11:33:48 +05:30
committed by BChoudhury-ms
parent eac5842176
commit 8698c6a3e2
39 changed files with 817 additions and 248 deletions

View File

@@ -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,

View 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" }),
}),
}),
);
});
});
});

View File

@@ -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);
}

View File

@@ -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");
});
});

View File

@@ -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();
}
}

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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,
},

View File

@@ -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",

View File

@@ -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,6 +69,12 @@ describe("CopyJobContext", () => {
databaseId: "",
containerId: "",
},
target: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
});
expect(contextValue.flow).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,8 +616,8 @@ 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", () => {

View File

@@ -23,14 +23,14 @@ const getInitialCopyJobState = (): CopyJobContextState => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
subscriptionId: userContext.subscriptionId || "",
account: userContext.databaseAccount || null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: userContext.subscriptionId || "",
account: userContext.databaseAccount || null,
subscription: null,
account: null,
databaseId: "",
containerId: "",
},

View File

@@ -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",

View File

@@ -85,13 +85,13 @@ 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",
@@ -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",
@@ -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",

View File

@@ -50,13 +50,13 @@ 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",

View File

@@ -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: "",
},
@@ -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: "",
},
@@ -398,7 +398,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscriptionId: "",
subscription: undefined,
databaseId: "",
containerId: "",
},
@@ -435,7 +435,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscription: undefined,
subscriptionId: "",
databaseId: "",
containerId: "",
},
@@ -476,7 +476,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscription: undefined,
subscriptionId: "",
databaseId: "",
containerId: "",
},
@@ -546,7 +546,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscriptionId: "",
subscription: undefined,
databaseId: "",
containerId: "",
},

View File

@@ -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();
});

View File

@@ -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>
);

View File

@@ -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"

View File

@@ -87,13 +87,13 @@ 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",
@@ -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,7 +285,7 @@ describe("PreviewCopyJob", () => {
const mockContext = createMockContext({
target: {
subscriptionId: "target-subscription-id",
subscription: mockSubscription,
account: targetAccount,
databaseId: "target-database",
containerId: "target-container",
@@ -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();
});
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -38,16 +38,16 @@ describe("AccountDropdown", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: {
subscriptionId: "test-subscription-id",
displayName: "Test Subscription",
},
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
subscription: {
subscriptionId: "test-subscription-id",
displayName: "Test Subscription",
},
account: null,
databaseId: "",
containerId: "",
@@ -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", () => {

View File

@@ -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

View File

@@ -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}>

View File

@@ -30,13 +30,13 @@ describe("SelectAccount", () => {
jobName: "",
migrationType: CopyJobMigrationType.Online,
source: {
subscription: null as any,
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "",
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
@@ -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();

View File

@@ -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"

View File

@@ -9,17 +9,7 @@ const createMockInitialState = (): CopyJobContextState => ({
migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: 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);
});
@@ -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);
});
@@ -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);
});

View File

@@ -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",

View File

@@ -71,13 +71,13 @@ describe("useSourceAndTargetData", () => {
migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: 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",

View File

@@ -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",

View File

@@ -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: "",

View File

@@ -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",
},
],

View File

@@ -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");
});
});

View File

@@ -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>

View File

@@ -57,13 +57,13 @@ export interface CopyJobContextState {
migrationType: CopyJobMigrationType;
sourceReadAccessFromTarget?: 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;

View 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);
});
});
});

View File

@@ -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)}`);

View File

@@ -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();
});
});
});

View File

@@ -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 } }}
/>
@@ -1062,6 +1065,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(),
@@ -1149,6 +1155,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);
@@ -1439,13 +1449,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) {
@@ -1474,7 +1487,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);