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,
|
databaseId: params.databaseId,
|
||||||
databaseLevelThroughput: params.databaseLevelThroughput,
|
databaseLevelThroughput: params.databaseLevelThroughput,
|
||||||
offerThroughput: params.offerThroughput,
|
offerThroughput: params.offerThroughput,
|
||||||
|
targetAccountOverride: params.targetAccountOverride,
|
||||||
};
|
};
|
||||||
await createDatabase(createDatabaseParams);
|
await createDatabase(createDatabaseParams);
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
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);
|
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||||
@@ -122,9 +123,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createResponse = await createUpdateSqlContainer(
|
const createResponse = await createUpdateSqlContainer(
|
||||||
userContext.subscriptionId,
|
params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId,
|
||||||
userContext.resourceGroup,
|
params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup,
|
||||||
userContext.databaseAccount.name,
|
params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name,
|
||||||
params.databaseId,
|
params.databaseId,
|
||||||
params.collectionId,
|
params.collectionId,
|
||||||
rpPayload,
|
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> {
|
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();
|
const databaseName = getDatabaseName().toLocaleLowerCase();
|
||||||
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
|
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,
|
options,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const createResponse = await createUpdateSqlDatabase(
|
const sub = params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
|
||||||
userContext.subscriptionId,
|
const rg = params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
|
||||||
userContext.resourceGroup,
|
const acct = params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name;
|
||||||
userContext.databaseAccount.name,
|
const createResponse = await createUpdateSqlDatabase(sub, rg, acct, params.databaseId, rpPayload);
|
||||||
params.databaseId,
|
|
||||||
rpPayload,
|
|
||||||
);
|
|
||||||
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
jest.mock("../../Utils/arm/request");
|
jest.mock("../../Utils/arm/request");
|
||||||
jest.mock("../CosmosClient");
|
jest.mock("../CosmosClient");
|
||||||
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
import { updateUserContext } from "../../UserContext";
|
import { updateUserContext } from "../../UserContext";
|
||||||
import { armRequest } from "../../Utils/arm/request";
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { readDatabases } from "./readDatabases";
|
import { readDatabases, readDatabasesForAccount } from "./readDatabases";
|
||||||
|
|
||||||
describe("readDatabases", () => {
|
describe("readDatabases", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -42,3 +43,64 @@ describe("readDatabases", () => {
|
|||||||
expect(client).toHaveBeenCalled();
|
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);
|
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;
|
targetMaxThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccountOverride {
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
accountName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateDatabaseParams {
|
export interface CreateDatabaseParams {
|
||||||
autoPilotMaxThroughput?: number;
|
autoPilotMaxThroughput?: number;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseLevelThroughput?: boolean;
|
databaseLevelThroughput?: boolean;
|
||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
|
targetAccountOverride?: AccountOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCollectionParamsBase {
|
export interface CreateCollectionParamsBase {
|
||||||
@@ -428,6 +435,7 @@ export interface CreateCollectionParamsBase {
|
|||||||
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
||||||
createNewDatabase: boolean;
|
createNewDatabase: boolean;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
|
targetAccountOverride?: AccountOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
||||||
|
|||||||
@@ -457,13 +457,13 @@ describe("CopyJobActions", () => {
|
|||||||
jobName: "test-job",
|
jobName: "test-job",
|
||||||
migrationType: "online" as any,
|
migrationType: "online" as any,
|
||||||
source: {
|
source: {
|
||||||
subscription: {} as any,
|
subscriptionId: "sub-123",
|
||||||
account: { id: "account-1", name: "source-account" } as any,
|
account: { id: "account-1", name: "source-account" } as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "sub-123",
|
subscription: {} as any,
|
||||||
account: { id: "account-1", name: "target-account" } as any,
|
account: { id: "account-1", name: "target-account" } as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -498,7 +498,7 @@ describe("CopyJobActions", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
|
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(mockRefreshJobList).toHaveBeenCalled();
|
||||||
expect(mockOnSuccess).toHaveBeenCalled();
|
expect(mockOnSuccess).toHaveBeenCalled();
|
||||||
@@ -509,13 +509,13 @@ describe("CopyJobActions", () => {
|
|||||||
jobName: "cross-account-job",
|
jobName: "cross-account-job",
|
||||||
migrationType: "offline" as any,
|
migrationType: "offline" as any,
|
||||||
source: {
|
source: {
|
||||||
subscription: {} as any,
|
subscriptionId: "sub-123",
|
||||||
account: { id: "account-1", name: "source-account" } as any,
|
account: { id: "account-1", name: "source-account" } as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "sub-456",
|
subscription: {} as any,
|
||||||
account: { id: "account-2", name: "target-account" } as any,
|
account: { id: "account-2", name: "target-account" } as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -528,7 +528,7 @@ describe("CopyJobActions", () => {
|
|||||||
await submitCreateCopyJob(mockState, mockOnSuccess);
|
await submitCreateCopyJob(mockState, mockOnSuccess);
|
||||||
|
|
||||||
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
|
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();
|
expect(mockOnSuccess).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -537,13 +537,13 @@ describe("CopyJobActions", () => {
|
|||||||
jobName: "failing-job",
|
jobName: "failing-job",
|
||||||
migrationType: "online" as any,
|
migrationType: "online" as any,
|
||||||
source: {
|
source: {
|
||||||
subscription: {} as any,
|
subscriptionId: "sub-123",
|
||||||
account: { id: "account-1", name: "source-account" } as any,
|
account: { id: "account-1", name: "source-account" } as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "sub-123",
|
subscription: {} as any,
|
||||||
account: { id: "account-1", name: "target-account" } as any,
|
account: { id: "account-1", name: "target-account" } as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -566,13 +566,13 @@ describe("CopyJobActions", () => {
|
|||||||
jobName: "test-job",
|
jobName: "test-job",
|
||||||
migrationType: "online" as any,
|
migrationType: "online" as any,
|
||||||
source: {
|
source: {
|
||||||
subscription: {} as any,
|
subscriptionId: "sub-123",
|
||||||
account: { id: "account-1", name: "source-account" } as any,
|
account: { id: "account-1", name: "source-account" } as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "sub-123",
|
subscription: {} as any,
|
||||||
account: { id: "account-1", name: "target-account" } as any,
|
account: { id: "account-1", name: "target-account" } as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
|
|||||||
@@ -137,12 +137,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
|
|||||||
properties: {
|
properties: {
|
||||||
source: {
|
source: {
|
||||||
component: "CosmosDBSql",
|
component: "CosmosDBSql",
|
||||||
...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }),
|
|
||||||
databaseName: source?.databaseId,
|
databaseName: source?.databaseId,
|
||||||
containerName: source?.containerId,
|
containerName: source?.containerId,
|
||||||
},
|
},
|
||||||
destination: {
|
destination: {
|
||||||
component: "CosmosDBSql",
|
component: "CosmosDBSql",
|
||||||
|
...(isSameAccount ? {} : { remoteAccountName: target?.account?.name }),
|
||||||
databaseName: target?.databaseId,
|
databaseName: target?.databaseId,
|
||||||
containerName: target?.containerId,
|
containerName: target?.containerId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ export default {
|
|||||||
createCopyJobPanelTitle: "Create copy job",
|
createCopyJobPanelTitle: "Create copy job",
|
||||||
|
|
||||||
// Select Account Screen
|
// Select Account Screen
|
||||||
selectAccountDescription: "Please select a source account from which to copy.",
|
selectAccountDescription: "Please select a destination account to copy to.",
|
||||||
subscriptionDropdownLabel: "Subscription",
|
subscriptionDropdownLabel: "Subscription",
|
||||||
subscriptionDropdownPlaceholder: "Select a subscription",
|
subscriptionDropdownPlaceholder: "Select a subscription",
|
||||||
sourceAccountDropdownLabel: "Account",
|
destinationAccountDropdownLabel: "Account",
|
||||||
sourceAccountDropdownPlaceholder: "Select an account",
|
destinationAccountDropdownPlaceholder: "Select an account",
|
||||||
migrationTypeOptions: {
|
migrationTypeOptions: {
|
||||||
offline: {
|
offline: {
|
||||||
title: "Offline mode",
|
title: "Offline mode",
|
||||||
@@ -47,14 +47,17 @@ export default {
|
|||||||
databaseDropdownPlaceholder: "Select a database",
|
databaseDropdownPlaceholder: "Select a database",
|
||||||
containerDropdownLabel: "Container",
|
containerDropdownLabel: "Container",
|
||||||
containerDropdownPlaceholder: "Select a 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",
|
createContainerButtonLabel: "Create a new container",
|
||||||
createContainerHeading: "Create new container",
|
createContainerHeading: "Create new container",
|
||||||
|
|
||||||
// Preview and Create Screen
|
// Preview and Create Screen
|
||||||
jobNameLabel: "Job name",
|
jobNameLabel: "Job name",
|
||||||
sourceSubscriptionLabel: "Source subscription",
|
destinationSubscriptionLabel: "Destination subscription",
|
||||||
sourceAccountLabel: "Source account",
|
destinationAccountLabel: "Destination account",
|
||||||
sourceDatabaseLabel: "Source database",
|
sourceDatabaseLabel: "Source database",
|
||||||
sourceContainerLabel: "Source container",
|
sourceContainerLabel: "Source container",
|
||||||
targetDatabaseLabel: "Destination database",
|
targetDatabaseLabel: "Destination database",
|
||||||
|
|||||||
@@ -59,12 +59,6 @@ describe("CopyJobContext", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null,
|
|
||||||
account: null,
|
|
||||||
databaseId: "",
|
|
||||||
containerId: "",
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
subscriptionId: "test-subscription-id",
|
subscriptionId: "test-subscription-id",
|
||||||
account: {
|
account: {
|
||||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||||
@@ -75,6 +69,12 @@ describe("CopyJobContext", () => {
|
|||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
|
target: {
|
||||||
|
subscription: null,
|
||||||
|
account: null,
|
||||||
|
databaseId: "",
|
||||||
|
containerId: "",
|
||||||
|
},
|
||||||
sourceReadAccessFromTarget: false,
|
sourceReadAccessFromTarget: false,
|
||||||
});
|
});
|
||||||
expect(contextValue.flow).toBeNull();
|
expect(contextValue.flow).toBeNull();
|
||||||
@@ -598,8 +598,8 @@ describe("CopyJobContext", () => {
|
|||||||
</CopyJobContextProvider>,
|
</CopyJobContextProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined();
|
expect(contextValue.copyJobState.source?.subscriptionId).toBe("test-subscription-id");
|
||||||
expect(contextValue.copyJobState.source?.account?.name).toBeUndefined();
|
expect(contextValue.copyJobState.source?.account?.name).toBe("test-account");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize target with userContext values", () => {
|
it("should initialize target with userContext values", () => {
|
||||||
@@ -616,8 +616,8 @@ describe("CopyJobContext", () => {
|
|||||||
</CopyJobContextProvider>,
|
</CopyJobContextProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id");
|
expect(contextValue.copyJobState.target.subscription).toBeNull();
|
||||||
expect(contextValue.copyJobState.target.account.name).toBe("test-account");
|
expect(contextValue.copyJobState.target.account).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize sourceReadAccessFromTarget as false", () => {
|
it("should initialize sourceReadAccessFromTarget as false", () => {
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ const getInitialCopyJobState = (): CopyJobContextState => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null,
|
subscriptionId: userContext.subscriptionId || "",
|
||||||
account: null,
|
account: userContext.databaseAccount || null,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: userContext.subscriptionId || "",
|
subscription: null,
|
||||||
account: userContext.databaseAccount || null,
|
account: null,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
|||||||
jobName: "test-job",
|
jobName: "test-job",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: { subscriptionId: "source-sub-id" } as Subscription,
|
subscriptionId: "source-sub-id",
|
||||||
account: {
|
account: {
|
||||||
id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
|
id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
|
||||||
name: "source-account",
|
name: "source-account",
|
||||||
@@ -101,7 +101,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
|
|||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "target-sub-id",
|
subscription: { subscriptionId: "target-sub-id" } as Subscription,
|
||||||
account: {
|
account: {
|
||||||
id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
|
id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
|
||||||
name: "target-account",
|
name: "target-account",
|
||||||
|
|||||||
@@ -85,13 +85,13 @@ describe("AssignPermissions Component", () => {
|
|||||||
jobName: "test-job",
|
jobName: "test-job",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: { subscriptionId: "source-sub" } as any,
|
subscriptionId: "source-sub",
|
||||||
account: { id: "source-account", name: "Source Account" } as any,
|
account: { id: "source-account", name: "Source Account" } as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "target-sub",
|
subscription: { subscriptionId: "target-sub" } as any,
|
||||||
account: { id: "target-account", name: "Target Account" } as any,
|
account: { id: "target-account", name: "Target Account" } as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -164,13 +164,13 @@ describe("AssignPermissions Component", () => {
|
|||||||
const copyJobState = createMockCopyJobState({
|
const copyJobState = createMockCopyJobState({
|
||||||
migrationType: CopyJobMigrationType.Online,
|
migrationType: CopyJobMigrationType.Online,
|
||||||
source: {
|
source: {
|
||||||
subscription: { subscriptionId: "same-sub" } as any,
|
subscriptionId: "same-sub",
|
||||||
account: { id: "same-account", name: "Same Account" } as any,
|
account: { id: "same-account", name: "Same Account" } as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "same-sub",
|
subscription: { subscriptionId: "same-sub" } as any,
|
||||||
account: { id: "same-account", name: "Same Account" } as any,
|
account: { id: "same-account", name: "Same Account" } as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -347,7 +347,7 @@ describe("AssignPermissions Component", () => {
|
|||||||
it("should handle missing account names", () => {
|
it("should handle missing account names", () => {
|
||||||
const copyJobState = createMockCopyJobState({
|
const copyJobState = createMockCopyJobState({
|
||||||
source: {
|
source: {
|
||||||
subscription: { subscriptionId: "source-sub" } as any,
|
subscriptionId: "source-sub",
|
||||||
account: { id: "source-account" } as any,
|
account: { id: "source-account" } as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ describe("PointInTimeRestore", () => {
|
|||||||
jobName: "test-job",
|
jobName: "test-job",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
|
subscriptionId: "test-sub",
|
||||||
account: mockSourceAccount,
|
account: mockSourceAccount,
|
||||||
databaseId: "test-db",
|
databaseId: "test-db",
|
||||||
containerId: "test-container",
|
containerId: "test-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "test-sub",
|
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
|
||||||
account: mockSourceAccount,
|
account: mockSourceAccount,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscription: undefined,
|
subscriptionId: "",
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -152,7 +152,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscriptionId: "",
|
subscription: undefined,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -208,7 +208,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscription: undefined,
|
subscriptionId: "",
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -222,7 +222,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscriptionId: "",
|
subscription: undefined,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -299,7 +299,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscriptionId: "",
|
subscription: undefined,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -337,7 +337,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscriptionId: "",
|
subscription: undefined,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -398,7 +398,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscriptionId: "",
|
subscription: undefined,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -435,7 +435,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscription: undefined,
|
subscriptionId: "",
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -476,7 +476,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscription: undefined,
|
subscriptionId: "",
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -546,7 +546,7 @@ describe("usePermissionsSection", () => {
|
|||||||
type: "",
|
type: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
subscriptionId: "",
|
subscription: undefined,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ describe("AddCollectionPanelWrapper", () => {
|
|||||||
expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument();
|
expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument();
|
||||||
expect(container.querySelector(".addCollectionPanelHeader")).toBeInTheDocument();
|
expect(container.querySelector(".addCollectionPanelHeader")).toBeInTheDocument();
|
||||||
expect(container.querySelector(".addCollectionPanelBody")).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();
|
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 Explorer from "Explorer/Explorer";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { produce } from "immer";
|
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 { AddCollectionPanel } from "../../../../Panes/AddCollectionPanel/AddCollectionPanel";
|
||||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||||
|
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||||
|
|
||||||
type AddCollectionPanelWrapperProps = {
|
type AddCollectionPanelWrapperProps = {
|
||||||
explorer?: Explorer;
|
explorer?: Explorer;
|
||||||
@@ -13,7 +16,26 @@ type AddCollectionPanelWrapperProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapperProps> = ({ explorer, goBack }) => {
|
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(() => {
|
useEffect(() => {
|
||||||
const sidePanelStore = useSidePanel.getState();
|
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(
|
const handleAddCollectionSuccess = useCallback(
|
||||||
(collectionData: { databaseId: string; collectionId: string }) => {
|
(collectionData: { databaseId: string; collectionId: string }) => {
|
||||||
setCopyJobState(
|
setCopyJobState(
|
||||||
@@ -38,13 +106,37 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
|||||||
[goBack],
|
[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 (
|
return (
|
||||||
<Stack className="addCollectionPanelWrapper">
|
<Stack className="addCollectionPanelWrapper">
|
||||||
<Stack.Item className="addCollectionPanelHeader">
|
<Stack.Item className="addCollectionPanelHeader">
|
||||||
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
<Text className="themeText">
|
||||||
|
{ContainerCopyMessages.createNewContainerSubHeading(targetAccountOverride?.accountName)}
|
||||||
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item className="addCollectionPanelBody">
|
<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.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`] = `
|
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="themeText css-117"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Configure the properties for the new container.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-StackItem addCollectionPanelBody css-110"
|
class="ms-StackItem addCollectionPanelBody css-116"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="add-collection-panel"
|
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`] = `
|
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with both props 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="themeText css-117"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Configure the properties for the new container.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-StackItem addCollectionPanelBody css-110"
|
class="ms-StackItem addCollectionPanelBody css-116"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="add-collection-panel"
|
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`] = `
|
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with explorer prop 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="themeText css-117"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Configure the properties for the new container.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-StackItem addCollectionPanelBody css-110"
|
class="ms-StackItem addCollectionPanelBody css-116"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="add-collection-panel"
|
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`] = `
|
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with goBack prop 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="themeText css-111"
|
class="themeText css-117"
|
||||||
>
|
>
|
||||||
Select the properties for your container.
|
Configure the properties for the new container.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ms-StackItem addCollectionPanelBody css-110"
|
class="ms-StackItem addCollectionPanelBody css-116"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="add-collection-panel"
|
data-testid="add-collection-panel"
|
||||||
|
|||||||
@@ -87,13 +87,13 @@ describe("PreviewCopyJob", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: mockSubscription,
|
subscriptionId: "test-subscription-id",
|
||||||
account: mockDatabaseAccount,
|
account: mockDatabaseAccount,
|
||||||
databaseId: "source-database",
|
databaseId: "source-database",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "test-subscription-id",
|
subscription: mockSubscription,
|
||||||
account: mockDatabaseAccount,
|
account: mockDatabaseAccount,
|
||||||
databaseId: "target-database",
|
databaseId: "target-database",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -146,7 +146,7 @@ describe("PreviewCopyJob", () => {
|
|||||||
it("should render with missing source subscription information", () => {
|
it("should render with missing source subscription information", () => {
|
||||||
const mockContext = createMockContext({
|
const mockContext = createMockContext({
|
||||||
source: {
|
source: {
|
||||||
subscription: undefined,
|
subscriptionId: "",
|
||||||
account: mockDatabaseAccount,
|
account: mockDatabaseAccount,
|
||||||
databaseId: "source-database",
|
databaseId: "source-database",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
@@ -165,7 +165,7 @@ describe("PreviewCopyJob", () => {
|
|||||||
it("should render with missing source account information", () => {
|
it("should render with missing source account information", () => {
|
||||||
const mockContext = createMockContext({
|
const mockContext = createMockContext({
|
||||||
source: {
|
source: {
|
||||||
subscription: mockSubscription,
|
subscriptionId: "test-subscription-id",
|
||||||
account: null,
|
account: null,
|
||||||
databaseId: "source-database",
|
databaseId: "source-database",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
@@ -184,13 +184,13 @@ describe("PreviewCopyJob", () => {
|
|||||||
it("should render with undefined database and container names", () => {
|
it("should render with undefined database and container names", () => {
|
||||||
const mockContext = createMockContext({
|
const mockContext = createMockContext({
|
||||||
source: {
|
source: {
|
||||||
subscription: mockSubscription,
|
subscriptionId: "test-subscription-id",
|
||||||
account: mockDatabaseAccount,
|
account: mockDatabaseAccount,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "test-subscription-id",
|
subscription: mockSubscription,
|
||||||
account: mockDatabaseAccount,
|
account: mockDatabaseAccount,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
@@ -219,7 +219,7 @@ describe("PreviewCopyJob", () => {
|
|||||||
|
|
||||||
const mockContext = createMockContext({
|
const mockContext = createMockContext({
|
||||||
source: {
|
source: {
|
||||||
subscription: longNameSubscription,
|
subscriptionId: longNameSubscription.subscriptionId,
|
||||||
account: longNameAccount,
|
account: longNameAccount,
|
||||||
databaseId: "long-database-name-for-testing-purposes",
|
databaseId: "long-database-name-for-testing-purposes",
|
||||||
containerId: "long-container-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", () => {
|
it("should handle special characters in database and container names", () => {
|
||||||
const mockContext = createMockContext({
|
const mockContext = createMockContext({
|
||||||
source: {
|
source: {
|
||||||
subscription: mockSubscription,
|
subscriptionId: "test-subscription-id",
|
||||||
account: mockDatabaseAccount,
|
account: mockDatabaseAccount,
|
||||||
databaseId: "test-db_with@special#chars",
|
databaseId: "test-db_with@special#chars",
|
||||||
containerId: "test-container_with@special#chars",
|
containerId: "test-container_with@special#chars",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "test-subscription-id",
|
subscription: mockSubscription,
|
||||||
account: mockDatabaseAccount,
|
account: mockDatabaseAccount,
|
||||||
databaseId: "target-db_with@special#chars",
|
databaseId: "target-db_with@special#chars",
|
||||||
containerId: "target-container_with@special#chars",
|
containerId: "target-container_with@special#chars",
|
||||||
@@ -285,7 +285,7 @@ describe("PreviewCopyJob", () => {
|
|||||||
|
|
||||||
const mockContext = createMockContext({
|
const mockContext = createMockContext({
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "target-subscription-id",
|
subscription: mockSubscription,
|
||||||
account: targetAccount,
|
account: targetAccount,
|
||||||
databaseId: "target-database",
|
databaseId: "target-database",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -360,7 +360,7 @@ describe("PreviewCopyJob", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(getByText(/Job name/i)).toBeInTheDocument();
|
expect(getByText(/Job name/i)).toBeInTheDocument();
|
||||||
expect(getByText(/Source subscription/i)).toBeInTheDocument();
|
expect(getByText(/Destination subscription/i)).toBeInTheDocument();
|
||||||
expect(getByText(/Source account/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} />
|
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.destinationSubscriptionLabel}</Text>
|
||||||
<Text data-test="source-subscription-name" className="themeText">
|
<Text data-test="destination-subscription-name" className="themeText">
|
||||||
{copyJobState.source?.subscription?.displayName}
|
{copyJobState.target?.subscription?.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.destinationAccountLabel}</Text>
|
||||||
<Text data-test="source-account-name" className="themeText">
|
<Text data-test="destination-account-name" className="themeText">
|
||||||
{copyJobState.source?.account?.name}
|
{copyJobState.target?.account?.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="destination-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -64,11 +64,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="destination-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -371,11 +371,11 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="destination-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -386,13 +386,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="destination-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
target-account
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -693,11 +693,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="destination-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -708,11 +708,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="destination-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -1015,13 +1015,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -1030,13 +1030,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -1337,11 +1337,11 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="destination-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1352,7 +1352,13 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="themeText css-125"
|
||||||
|
data-test="destination-account-name"
|
||||||
|
>
|
||||||
|
test-account
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -1653,7 +1659,13 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="themeText css-125"
|
||||||
|
data-test="destination-subscription-name"
|
||||||
|
>
|
||||||
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -1662,11 +1674,11 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="destination-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -1969,11 +1981,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="destination-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -1984,11 +1996,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="destination-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -2291,11 +2303,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="destination-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -2306,11 +2318,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="destination-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
@@ -2613,11 +2625,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source subscription
|
Destination subscription
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-subscription-name"
|
data-test="destination-subscription-name"
|
||||||
>
|
>
|
||||||
Test Subscription
|
Test Subscription
|
||||||
</span>
|
</span>
|
||||||
@@ -2628,11 +2640,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
|||||||
<span
|
<span
|
||||||
class="bold themeText css-125"
|
class="bold themeText css-125"
|
||||||
>
|
>
|
||||||
Source account
|
Destination account
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="themeText css-125"
|
class="themeText css-125"
|
||||||
data-test="source-account-name"
|
data-test="destination-account-name"
|
||||||
>
|
>
|
||||||
test-account
|
test-account
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ describe("AccountDropdown", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: {
|
subscriptionId: "",
|
||||||
subscriptionId: "test-subscription-id",
|
|
||||||
displayName: "Test Subscription",
|
|
||||||
},
|
|
||||||
account: null,
|
account: null,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: {
|
||||||
|
subscriptionId: "test-subscription-id",
|
||||||
|
displayName: "Test Subscription",
|
||||||
|
},
|
||||||
account: null,
|
account: null,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
@@ -129,11 +129,11 @@ describe("AccountDropdown", () => {
|
|||||||
renderWithContext();
|
renderWithContext();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(`${ContainerCopyMessages.sourceAccountDropdownLabel}:`, { exact: true }),
|
screen.getByText(`${ContainerCopyMessages.destinationAccountDropdownLabel}:`, { exact: true }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByRole("combobox")).toHaveAttribute(
|
expect(screen.getByRole("combobox")).toHaveAttribute(
|
||||||
"aria-label",
|
"aria-label",
|
||||||
ContainerCopyMessages.sourceAccountDropdownLabel,
|
ContainerCopyMessages.destinationAccountDropdownLabel,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(mockCopyJobState);
|
const newState = stateUpdateFunction(mockCopyJobState);
|
||||||
expect(newState.source.account).toEqual({
|
expect(newState.target.account).toEqual({
|
||||||
...mockDatabaseAccount1,
|
...mockDatabaseAccount1,
|
||||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||||
});
|
});
|
||||||
@@ -226,20 +226,21 @@ describe("AccountDropdown", () => {
|
|||||||
|
|
||||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||||
const newState = stateUpdateFunction(mockCopyJobState);
|
const newState = stateUpdateFunction(mockCopyJobState);
|
||||||
expect(newState.source.account).toEqual({
|
expect(newState.target.account).toEqual({
|
||||||
...mockDatabaseAccount2,
|
...mockDatabaseAccount2,
|
||||||
id: normalizeAccountId(mockDatabaseAccount2.id),
|
id: normalizeAccountId(mockDatabaseAccount2.id),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should keep current account if it exists in the filtered list", async () => {
|
it("should keep current account if it exists in the filtered list", async () => {
|
||||||
|
const normalizedAccount1 = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
|
||||||
const contextWithSelectedAccount = {
|
const contextWithSelectedAccount = {
|
||||||
...mockCopyJobContextValue,
|
...mockCopyJobContextValue,
|
||||||
copyJobState: {
|
copyJobState: {
|
||||||
...mockCopyJobState,
|
...mockCopyJobState,
|
||||||
source: {
|
target: {
|
||||||
...mockCopyJobState.source,
|
...mockCopyJobState.target,
|
||||||
account: mockDatabaseAccount1,
|
account: normalizedAccount1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -256,12 +257,9 @@ describe("AccountDropdown", () => {
|
|||||||
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
||||||
expect(newState).toEqual({
|
expect(newState).toEqual({
|
||||||
...contextWithSelectedAccount.copyJobState,
|
...contextWithSelectedAccount.copyJobState,
|
||||||
source: {
|
target: {
|
||||||
...contextWithSelectedAccount.copyJobState.source,
|
...contextWithSelectedAccount.copyJobState.target,
|
||||||
account: {
|
account: normalizedAccount1,
|
||||||
...mockDatabaseAccount1,
|
|
||||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -297,8 +295,8 @@ describe("AccountDropdown", () => {
|
|||||||
...mockCopyJobContextValue,
|
...mockCopyJobContextValue,
|
||||||
copyJobState: {
|
copyJobState: {
|
||||||
...mockCopyJobState,
|
...mockCopyJobState,
|
||||||
source: {
|
target: {
|
||||||
...mockCopyJobState.source,
|
...mockCopyJobState.target,
|
||||||
account: portalAccount,
|
account: portalAccount,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -323,8 +321,8 @@ describe("AccountDropdown", () => {
|
|||||||
...mockCopyJobContextValue,
|
...mockCopyJobContextValue,
|
||||||
copyJobState: {
|
copyJobState: {
|
||||||
...mockCopyJobState,
|
...mockCopyJobState,
|
||||||
source: {
|
target: {
|
||||||
...mockCopyJobState.source,
|
...mockCopyJobState.target,
|
||||||
account: hostedAccount,
|
account: hostedAccount,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -361,8 +359,8 @@ describe("AccountDropdown", () => {
|
|||||||
...mockCopyJobContextValue,
|
...mockCopyJobContextValue,
|
||||||
copyJobState: {
|
copyJobState: {
|
||||||
...mockCopyJobState,
|
...mockCopyJobState,
|
||||||
source: {
|
target: {
|
||||||
...mockCopyJobState.source,
|
...mockCopyJobState.target,
|
||||||
subscription: null,
|
subscription: null,
|
||||||
},
|
},
|
||||||
} as CopyJobContextState,
|
} as CopyJobContextState,
|
||||||
@@ -376,13 +374,13 @@ describe("AccountDropdown", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not update state if account is already selected and the same", async () => {
|
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 = {
|
const contextWithSelectedAccount = {
|
||||||
...mockCopyJobContextValue,
|
...mockCopyJobContextValue,
|
||||||
copyJobState: {
|
copyJobState: {
|
||||||
...mockCopyJobState,
|
...mockCopyJobState,
|
||||||
source: {
|
target: {
|
||||||
...mockCopyJobState.source,
|
...mockCopyJobState.target,
|
||||||
account: selectedAccount,
|
account: selectedAccount,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -409,7 +407,7 @@ describe("AccountDropdown", () => {
|
|||||||
renderWithContext();
|
renderWithContext();
|
||||||
|
|
||||||
const dropdown = screen.getByRole("combobox");
|
const dropdown = screen.getByRole("combobox");
|
||||||
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.sourceAccountDropdownLabel);
|
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.destinationAccountDropdownLabel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have required attribute", () => {
|
it("should have required attribute", () => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const normalizeAccountId = (id: string = "") => {
|
|||||||
export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||||
|
|
||||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||||
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
||||||
const sqlApiOnlyAccounts = (allAccounts || [])
|
const sqlApiOnlyAccounts = (allAccounts || [])
|
||||||
.filter((account) => apiType(account) === "SQL")
|
.filter((account) => apiType(account) === "SQL")
|
||||||
@@ -36,11 +36,11 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
|
|
||||||
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
||||||
setCopyJobState((prevState) => {
|
setCopyJobState((prevState) => {
|
||||||
if (prevState.source?.account?.id !== newAccount.id) {
|
if (prevState.target?.account?.id !== newAccount.id) {
|
||||||
return {
|
return {
|
||||||
...prevState,
|
...prevState,
|
||||||
source: {
|
target: {
|
||||||
...prevState.source,
|
...prevState.target,
|
||||||
account: newAccount,
|
account: newAccount,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -51,13 +51,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
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 predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
|
||||||
const selectedAccountId = currentAccountId || predefinedAccountId;
|
const selectedAccountId = currentAccountId || predefinedAccountId;
|
||||||
|
|
||||||
const targetAccount: DatabaseAccount | null =
|
const matchedAccount: DatabaseAccount | null =
|
||||||
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
|
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
|
||||||
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
|
updateCopyJobState(matchedAccount || sqlApiOnlyAccounts[0]);
|
||||||
}
|
}
|
||||||
}, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]);
|
}, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]);
|
||||||
|
|
||||||
@@ -77,13 +77,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0;
|
const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0;
|
||||||
const selectedAccountId = normalizeAccountId(copyJobState?.source?.account?.id ?? "");
|
const selectedAccountId = normalizeAccountId(copyJobState?.target?.account?.id ?? "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}>
|
<FieldRow label={ContainerCopyMessages.destinationAccountDropdownLabel}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder}
|
placeholder={ContainerCopyMessages.destinationAccountDropdownPlaceholder}
|
||||||
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel}
|
ariaLabel={ContainerCopyMessages.destinationAccountDropdownLabel}
|
||||||
options={accountOptions}
|
options={accountOptions}
|
||||||
disabled={isAccountDropdownDisabled}
|
disabled={isAccountDropdownDisabled}
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
|
|||||||
|
|
||||||
const updateCopyJobState = (newSubscription: Subscription) => {
|
const updateCopyJobState = (newSubscription: Subscription) => {
|
||||||
setCopyJobState((prevState) => {
|
setCopyJobState((prevState) => {
|
||||||
if (prevState.source?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
|
if (prevState.target?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
|
||||||
return {
|
return {
|
||||||
...prevState,
|
...prevState,
|
||||||
source: {
|
target: {
|
||||||
...prevState.source,
|
...prevState.target,
|
||||||
subscription: newSubscription,
|
subscription: newSubscription,
|
||||||
account: null,
|
account: null,
|
||||||
},
|
},
|
||||||
@@ -33,7 +33,7 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (subscriptions && subscriptions.length > 0) {
|
if (subscriptions && subscriptions.length > 0) {
|
||||||
const currentSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
const currentSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||||
const predefinedSubscriptionId = userContext.subscriptionId;
|
const predefinedSubscriptionId = userContext.subscriptionId;
|
||||||
const selectedSubscriptionId = currentSubscriptionId || predefinedSubscriptionId;
|
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 (
|
return (
|
||||||
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}>
|
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}>
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ describe("SelectAccount", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Online,
|
migrationType: CopyJobMigrationType.Online,
|
||||||
source: {
|
source: {
|
||||||
subscription: null as any,
|
subscriptionId: "",
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: null as any,
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
@@ -68,7 +68,7 @@ describe("SelectAccount", () => {
|
|||||||
expect(container.firstChild).toHaveAttribute("data-test", "Panel:SelectAccountContainer");
|
expect(container.firstChild).toHaveAttribute("data-test", "Panel:SelectAccountContainer");
|
||||||
expect(container.firstChild).toHaveClass("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("subscription-dropdown")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();
|
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
|||||||
<span
|
<span
|
||||||
class="themeText css-110"
|
class="themeText css-110"
|
||||||
>
|
>
|
||||||
Please select a source account from which to copy.
|
Please select a destination account to copy to.
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
data-testid="subscription-dropdown"
|
data-testid="subscription-dropdown"
|
||||||
|
|||||||
@@ -9,17 +9,7 @@ const createMockInitialState = (): CopyJobContextState => ({
|
|||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
sourceReadAccessFromTarget: false,
|
sourceReadAccessFromTarget: false,
|
||||||
source: {
|
source: {
|
||||||
subscription: {
|
subscriptionId: "source-sub-id",
|
||||||
subscriptionId: "source-sub-id",
|
|
||||||
displayName: "Source Subscription",
|
|
||||||
state: "Enabled",
|
|
||||||
subscriptionPolicies: {
|
|
||||||
locationPlacementId: "test",
|
|
||||||
quotaId: "test",
|
|
||||||
spendingLimit: "Off",
|
|
||||||
},
|
|
||||||
authorizationSource: "test",
|
|
||||||
},
|
|
||||||
account: {
|
account: {
|
||||||
id: "source-account-id",
|
id: "source-account-id",
|
||||||
name: "source-account",
|
name: "source-account",
|
||||||
@@ -50,7 +40,17 @@ const createMockInitialState = (): CopyJobContextState => ({
|
|||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
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: {
|
account: {
|
||||||
id: "target-account-id",
|
id: "target-account-id",
|
||||||
name: "target-account",
|
name: "target-account",
|
||||||
@@ -169,7 +169,7 @@ describe("dropDownChangeHandler", () => {
|
|||||||
|
|
||||||
expect(capturedState.source.databaseId).toBe("new-source-db");
|
expect(capturedState.source.databaseId).toBe("new-source-db");
|
||||||
expect(capturedState.source.containerId).toBeUndefined();
|
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.source.account).toEqual(initialState.source.account);
|
||||||
expect(capturedState.target).toEqual(initialState.target);
|
expect(capturedState.target).toEqual(initialState.target);
|
||||||
});
|
});
|
||||||
@@ -193,7 +193,7 @@ describe("dropDownChangeHandler", () => {
|
|||||||
|
|
||||||
expect(capturedState.source.containerId).toBe("new-source-container");
|
expect(capturedState.source.containerId).toBe("new-source-container");
|
||||||
expect(capturedState.source.databaseId).toBe(initialState.source.databaseId);
|
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.source.account).toEqual(initialState.source.account);
|
||||||
expect(capturedState.target).toEqual(initialState.target);
|
expect(capturedState.target).toEqual(initialState.target);
|
||||||
});
|
});
|
||||||
@@ -215,7 +215,7 @@ describe("dropDownChangeHandler", () => {
|
|||||||
|
|
||||||
expect(capturedState.target.databaseId).toBe("new-target-db");
|
expect(capturedState.target.databaseId).toBe("new-target-db");
|
||||||
expect(capturedState.target.containerId).toBeUndefined();
|
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.target.account).toEqual(initialState.target.account);
|
||||||
expect(capturedState.source).toEqual(initialState.source);
|
expect(capturedState.source).toEqual(initialState.source);
|
||||||
});
|
});
|
||||||
@@ -239,7 +239,7 @@ describe("dropDownChangeHandler", () => {
|
|||||||
|
|
||||||
expect(capturedState.target.containerId).toBe("new-target-container");
|
expect(capturedState.target.containerId).toBe("new-target-container");
|
||||||
expect(capturedState.target.databaseId).toBe(initialState.target.databaseId);
|
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.target.account).toEqual(initialState.target.account);
|
||||||
expect(capturedState.source).toEqual(initialState.source);
|
expect(capturedState.source).toEqual(initialState.source);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: { subscriptionId: "test-subscription-id" },
|
subscriptionId: "test-subscription-id",
|
||||||
account: {
|
account: {
|
||||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||||
name: "test-account",
|
name: "test-account",
|
||||||
@@ -82,7 +82,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
|||||||
containerId: "container1",
|
containerId: "container1",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "test-subscription-id",
|
subscription: { subscriptionId: "test-subscription-id" },
|
||||||
account: {
|
account: {
|
||||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||||
name: "test-account",
|
name: "test-account",
|
||||||
|
|||||||
@@ -71,13 +71,13 @@ describe("useSourceAndTargetData", () => {
|
|||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
sourceReadAccessFromTarget: false,
|
sourceReadAccessFromTarget: false,
|
||||||
source: {
|
source: {
|
||||||
subscription: mockSubscription,
|
subscriptionId: "source-subscription-id",
|
||||||
account: mockSourceAccount,
|
account: mockSourceAccount,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "target-subscription-id",
|
subscription: mockSubscription,
|
||||||
account: mockTargetAccount,
|
account: mockTargetAccount,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
|
|||||||
@@ -86,13 +86,13 @@ describe("useCopyJobNavigation", () => {
|
|||||||
jobName: "test-job",
|
jobName: "test-job",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: { subscriptionId: "source-sub-id" } as any,
|
subscriptionId: "source-sub-id",
|
||||||
account: { id: "source-account-id", name: "Account-1" } as any,
|
account: { id: "source-account-id", name: "Account-1" } as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "target-sub-id",
|
subscription: { subscriptionId: "target-sub-id" } as any,
|
||||||
account: { id: "target-account-id", name: "Account-2" } as any,
|
account: { id: "target-account-id", name: "Account-2" } as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
|
|||||||
@@ -142,14 +142,14 @@ describe("useCreateCopyJobScreensList", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: { subscriptionId: "test-sub" } as any,
|
subscriptionId: "test-sub",
|
||||||
account: { name: "test-account" } as any,
|
account: { name: "test-account" } as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: { subscriptionId: "test-sub" } as any,
|
||||||
account: null as any,
|
account: { name: "test-account" } as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -171,14 +171,14 @@ describe("useCreateCopyJobScreensList", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null as any,
|
subscriptionId: "",
|
||||||
account: { name: "test-account" } as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: null as any,
|
||||||
account: null as any,
|
account: { name: "test-account" } as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
@@ -210,13 +210,13 @@ describe("useCreateCopyJobScreensList", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null as any,
|
subscriptionId: "",
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "source-db",
|
databaseId: "source-db",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: null as any,
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -240,13 +240,13 @@ describe("useCreateCopyJobScreensList", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null as any,
|
subscriptionId: "",
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "source-container",
|
containerId: "source-container",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: null as any,
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "target-db",
|
databaseId: "target-db",
|
||||||
containerId: "target-container",
|
containerId: "target-container",
|
||||||
@@ -288,13 +288,13 @@ describe("useCreateCopyJobScreensList", () => {
|
|||||||
jobName: "valid-job-name_123",
|
jobName: "valid-job-name_123",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null as any,
|
subscriptionId: "",
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: null as any,
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
@@ -318,13 +318,13 @@ describe("useCreateCopyJobScreensList", () => {
|
|||||||
jobName: "invalid job name with spaces!",
|
jobName: "invalid job name with spaces!",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null as any,
|
subscriptionId: "",
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: null as any,
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
@@ -348,13 +348,13 @@ describe("useCreateCopyJobScreensList", () => {
|
|||||||
jobName: "",
|
jobName: "",
|
||||||
migrationType: CopyJobMigrationType.Offline,
|
migrationType: CopyJobMigrationType.Offline,
|
||||||
source: {
|
source: {
|
||||||
subscription: null as any,
|
subscriptionId: "",
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: "",
|
subscription: null as any,
|
||||||
account: null as any,
|
account: null as any,
|
||||||
databaseId: "",
|
databaseId: "",
|
||||||
containerId: "",
|
containerId: "",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function useCreateCopyJobScreensList(goBack: () => void): Screen[] {
|
|||||||
component: <SelectAccount />,
|
component: <SelectAccount />,
|
||||||
validations: [
|
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",
|
message: "Please select a subscription and account to proceed",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
|||||||
sourceContainerLabel: "Source Container",
|
sourceContainerLabel: "Source Container",
|
||||||
targetDatabaseLabel: "Destination Database",
|
targetDatabaseLabel: "Destination Database",
|
||||||
targetContainerLabel: "Destination Container",
|
targetContainerLabel: "Destination Container",
|
||||||
sourceAccountLabel: "Source Account",
|
destinationAccountLabel: "Destination account",
|
||||||
MonitorJobs: {
|
MonitorJobs: {
|
||||||
Columns: {
|
Columns: {
|
||||||
lastUpdatedTime: "Date & time",
|
lastUpdatedTime: "Date & time",
|
||||||
@@ -102,8 +102,8 @@ describe("CopyJobDetails", () => {
|
|||||||
expect(screen.getByText("Date & time")).toBeInTheDocument();
|
expect(screen.getByText("Date & time")).toBeInTheDocument();
|
||||||
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
|
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByText("Source Account")).toBeInTheDocument();
|
expect(screen.getByText("Destination account")).toBeInTheDocument();
|
||||||
expect(screen.getByText("sourceAccount")).toBeInTheDocument();
|
expect(screen.getByText("targetAccount")).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByText("Mode")).toBeInTheDocument();
|
expect(screen.getByText("Mode")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Offline")).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_source_container_with_underscores")).toBeInTheDocument();
|
||||||
expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument();
|
expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument();
|
||||||
expect(screen.getByText("complex_target_container_with_underscores")).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} />);
|
render(<CopyJobDetails job={mockBasicJob} />);
|
||||||
|
|
||||||
const dateTimeHeading = screen.getByText("Date & time");
|
const dateTimeHeading = screen.getByText("Date & time");
|
||||||
const sourceAccountHeading = screen.getByText("Source Account");
|
const destinationAccountHeading = screen.getByText("Destination account");
|
||||||
const modeHeading = screen.getByText("Mode");
|
const modeHeading = screen.getByText("Mode");
|
||||||
|
|
||||||
expect(dateTimeHeading).toHaveClass("bold");
|
expect(dateTimeHeading).toHaveClass("bold");
|
||||||
expect(sourceAccountHeading).toHaveClass("bold");
|
expect(destinationAccountHeading).toHaveClass("bold");
|
||||||
expect(modeHeading).toHaveClass("bold");
|
expect(modeHeading).toHaveClass("bold");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
|||||||
<Text className="themeText">{job.LastUpdatedTime}</Text>
|
<Text className="themeText">{job.LastUpdatedTime}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.destinationAccountLabel}</Text>
|
||||||
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
|
<Text className="themeText">{job.Destination?.remoteAccountName}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item style={sectionCss.verticalAlign}>
|
<Stack.Item style={sectionCss.verticalAlign}>
|
||||||
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ export interface CopyJobContextState {
|
|||||||
migrationType: CopyJobMigrationType;
|
migrationType: CopyJobMigrationType;
|
||||||
sourceReadAccessFromTarget?: boolean;
|
sourceReadAccessFromTarget?: boolean;
|
||||||
source: {
|
source: {
|
||||||
subscription: Subscription | null;
|
subscriptionId: string;
|
||||||
account: DatabaseAccount | null;
|
account: DatabaseAccount | null;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
containerId: string;
|
containerId: string;
|
||||||
};
|
};
|
||||||
target: {
|
target: {
|
||||||
subscriptionId: string;
|
subscription: Subscription | null;
|
||||||
account: DatabaseAccount | null;
|
account: DatabaseAccount | null;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
containerId: 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();
|
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 = {
|
const addSynapseLinkDialogProps: DialogProps = {
|
||||||
linkProps: {
|
linkProps: {
|
||||||
linkText: "Learn more",
|
linkText: "Learn more",
|
||||||
@@ -249,7 +253,7 @@ export default class Explorer {
|
|||||||
useDialog.getState().closeDialog();
|
useDialog.getState().closeDialog();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await update(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, {
|
await update(subscriptionId, resourceGroup, accountName, {
|
||||||
properties: {
|
properties: {
|
||||||
enableAnalyticalStorage: true,
|
enableAnalyticalStorage: true,
|
||||||
},
|
},
|
||||||
@@ -258,7 +262,9 @@ export default class Explorer {
|
|||||||
clearInProgressMessage();
|
clearInProgressMessage();
|
||||||
logConsoleInfo("Enabled Azure Synapse Link for this account");
|
logConsoleInfo("Enabled Azure Synapse Link for this account");
|
||||||
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
|
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
|
||||||
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
|
if (!targetAccountOverride) {
|
||||||
|
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearInProgressMessage();
|
clearInProgressMessage();
|
||||||
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
|
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
|
||||||
|
|||||||
@@ -12,4 +12,56 @@ describe("AddCollectionPanel", () => {
|
|||||||
const wrapper = shallow(<AddCollectionPanel {...props} />);
|
const wrapper = shallow(<AddCollectionPanel {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
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 { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
|
import { AccountOverride } from "Contracts/DataModels";
|
||||||
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||||
import {
|
import {
|
||||||
@@ -68,6 +69,8 @@ export interface AddCollectionPanelProps {
|
|||||||
isQuickstart?: boolean;
|
isQuickstart?: boolean;
|
||||||
isCopyJobFlow?: boolean;
|
isCopyJobFlow?: boolean;
|
||||||
onSubmitSuccess?: (collectionData: { databaseId: string; collectionId: string }) => void;
|
onSubmitSuccess?: (collectionData: { databaseId: string; collectionId: string }) => void;
|
||||||
|
targetAccountOverride?: AccountOverride;
|
||||||
|
externalDatabaseOptions?: IDropdownOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||||
@@ -876,7 +879,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Text>
|
</Text>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
text={t(Keys.panes.addCollection.enable)}
|
text={t(Keys.panes.addCollection.enable)}
|
||||||
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
|
onClick={() => this.props.explorer.openEnableSynapseLinkDialog(this.props.targetAccountOverride)}
|
||||||
style={{ height: 27, width: 80 }}
|
style={{ height: 27, width: 80 }}
|
||||||
styles={{ label: { fontSize: 12 } }}
|
styles={{ label: { fontSize: 12 } }}
|
||||||
/>
|
/>
|
||||||
@@ -1062,6 +1065,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getDatabaseOptions(): IDropdownOption[] {
|
private getDatabaseOptions(): IDropdownOption[] {
|
||||||
|
if (this.props.externalDatabaseOptions) {
|
||||||
|
return this.props.externalDatabaseOptions;
|
||||||
|
}
|
||||||
return useDatabases.getState().databases?.map((database) => ({
|
return useDatabases.getState().databases?.map((database) => ({
|
||||||
key: database.id(),
|
key: database.id(),
|
||||||
text: database.id(),
|
text: database.id(),
|
||||||
@@ -1149,6 +1155,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.targetAccountOverride) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const selectedDatabase = useDatabases
|
const selectedDatabase = useDatabases
|
||||||
.getState()
|
.getState()
|
||||||
.databases?.find((database) => database.id() === this.state.selectedDatabaseId);
|
.databases?.find((database) => database.id() === this.state.selectedDatabaseId);
|
||||||
@@ -1439,13 +1449,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
||||||
vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy,
|
||||||
fullTextPolicy: this.state.fullTextPolicy,
|
fullTextPolicy: this.state.fullTextPolicy,
|
||||||
|
targetAccountOverride: this.props.targetAccountOverride,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({ isExecuting: true });
|
this.setState({ isExecuting: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createCollection(createCollectionParams);
|
await createCollection(createCollectionParams);
|
||||||
await this.props.explorer.refreshAllDatabases();
|
if (!this.props.isCopyJobFlow) {
|
||||||
|
await this.props.explorer.refreshAllDatabases();
|
||||||
|
}
|
||||||
if (this.props.isQuickstart) {
|
if (this.props.isQuickstart) {
|
||||||
const database = useDatabases.getState().findDatabaseWithId(databaseId);
|
const database = useDatabases.getState().findDatabaseWithId(databaseId);
|
||||||
if (database) {
|
if (database) {
|
||||||
@@ -1474,7 +1487,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
useSidePanel.getState().closeSidePanel();
|
useSidePanel.getState().closeSidePanel();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 });
|
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });
|
||||||
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
||||||
TelemetryProcessor.traceFailure(Action.CreateCollection, failureTelemetryData, startKey);
|
TelemetryProcessor.traceFailure(Action.CreateCollection, failureTelemetryData, startKey);
|
||||||
|
|||||||
Reference in New Issue
Block a user