mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-19 04:48:59 +01:00
feat: Redesign container-copy flow to select destination account and enable cross-account container creation
This commit is contained in:
committed by
BChoudhury-ms
parent
eac5842176
commit
8698c6a3e2
@@ -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",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
|
||||
@@ -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,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
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 } }}
|
||||
/>
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user