diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 7f531211c..91031034b 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -34,6 +34,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams databaseId: params.databaseId, databaseLevelThroughput: params.databaseLevelThroughput, offerThroughput: params.offerThroughput, + targetAccountOverride: params.targetAccountOverride, }; await createDatabase(createDatabaseParams); } @@ -63,7 +64,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams }; const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise => { - if (!params.createNewDatabase) { + if (!params.createNewDatabase && !params.targetAccountOverride) { const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId); if (!isValid) { const collectionName = getCollectionName().toLocaleLowerCase(); @@ -122,9 +123,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr }; const createResponse = await createUpdateSqlContainer( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, + params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId, + params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup, + params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name, params.databaseId, params.collectionId, rpPayload, diff --git a/src/Common/dataAccess/createDatabase.test.ts b/src/Common/dataAccess/createDatabase.test.ts new file mode 100644 index 000000000..3fca00378 --- /dev/null +++ b/src/Common/dataAccess/createDatabase.test.ts @@ -0,0 +1,137 @@ +jest.mock("../../Utils/arm/request"); +jest.mock("../CosmosClient"); +jest.mock("../../Utils/arm/generatedClients/cosmos/sqlResources"); + +import ko from "knockout"; +import { AuthType } from "../../AuthType"; +import { CreateDatabaseParams, DatabaseAccount } from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { useDatabases } from "../../Explorer/useDatabases"; +import { updateUserContext } from "../../UserContext"; +import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { SqlDatabaseGetResults } from "../../Utils/arm/generatedClients/cosmos/types"; +import { createDatabase } from "./createDatabase"; + +const mockCreateUpdateSqlDatabase = createUpdateSqlDatabase as jest.MockedFunction; + +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); + }); + + it("should call ARM createUpdateSqlDatabase when logged in with AAD", async () => { + await createDatabase({ databaseId: "testDb" }); + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled(); + }); + + describe("targetAccountOverride behavior", () => { + it("should use targetAccountOverride subscriptionId, resourceGroup, and accountName for SQL DB creation", async () => { + const params: CreateDatabaseParams = { + databaseId: "testDb", + targetAccountOverride: { + subscriptionId: "override-sub", + resourceGroup: "override-rg", + accountName: "override-account", + capabilities: [], + }, + }; + + await createDatabase(params); + + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith( + "override-sub", + "override-rg", + "override-account", + "testDb", + expect.any(Object), + ); + }); + + it("should use userContext values when targetAccountOverride is not provided", async () => { + await createDatabase({ databaseId: "testDb" }); + + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith( + "default-subscription", + "default-rg", + "default-account", + "testDb", + expect.any(Object), + ); + }); + + it("should skip validateDatabaseId check when targetAccountOverride is provided", async () => { + // Simulate database already existing — validateDatabaseId returns false + useDatabases.setState({ + databases: [{ id: ko.observable("testDb") } as unknown as ViewModels.Database], + validateDatabaseId: () => false, + } as unknown as ReturnType); + + const params: CreateDatabaseParams = { + databaseId: "testDb", + targetAccountOverride: { + subscriptionId: "override-sub", + resourceGroup: "override-rg", + accountName: "override-account", + capabilities: [], + }, + }; + + // Should NOT throw even though the normal duplicate check would fail + await expect(createDatabase(params)).resolves.not.toThrow(); + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled(); + }); + + it("should throw if validateDatabaseId returns false and no targetAccountOverride is set", async () => { + useDatabases.setState({ + databases: [{ id: ko.observable("existingDb") } as unknown as ViewModels.Database], + validateDatabaseId: () => false, + } as unknown as ReturnType); + + await expect(createDatabase({ databaseId: "existingDb" })).rejects.toThrow(); + expect(mockCreateUpdateSqlDatabase).not.toHaveBeenCalled(); + }); + + it("should pass databaseId in request payload regardless of targetAccountOverride", async () => { + const params: CreateDatabaseParams = { + databaseId: "my-database", + targetAccountOverride: { + subscriptionId: "any-sub", + resourceGroup: "any-rg", + accountName: "any-account", + capabilities: [], + }, + }; + + await createDatabase(params); + + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.any(String), + "my-database", + expect.objectContaining({ + properties: expect.objectContaining({ + resource: expect.objectContaining({ id: "my-database" }), + }), + }), + ); + }); + }); +}); diff --git a/src/Common/dataAccess/createDatabase.ts b/src/Common/dataAccess/createDatabase.ts index c562c4068..31190cfe7 100644 --- a/src/Common/dataAccess/createDatabase.ts +++ b/src/Common/dataAccess/createDatabase.ts @@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P } async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise { - if (!useDatabases.getState().validateDatabaseId(params.databaseId)) { + if (!params.targetAccountOverride && !useDatabases.getState().validateDatabaseId(params.databaseId)) { const databaseName = getDatabaseName().toLocaleLowerCase(); throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`); } @@ -72,13 +72,10 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi options, }, }; - const createResponse = await createUpdateSqlDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - rpPayload, - ); + const sub = params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId; + const rg = params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup; + const acct = params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name; + const createResponse = await createUpdateSqlDatabase(sub, rg, acct, params.databaseId, rpPayload); return createResponse && (createResponse.properties.resource as DataModels.Database); } diff --git a/src/Common/dataAccess/readDatabases.test.ts b/src/Common/dataAccess/readDatabases.test.ts index 8f6368f78..0d0eb90fc 100644 --- a/src/Common/dataAccess/readDatabases.test.ts +++ b/src/Common/dataAccess/readDatabases.test.ts @@ -1,11 +1,12 @@ jest.mock("../../Utils/arm/request"); jest.mock("../CosmosClient"); + import { AuthType } from "../../AuthType"; import { DatabaseAccount } from "../../Contracts/DataModels"; import { updateUserContext } from "../../UserContext"; import { armRequest } from "../../Utils/arm/request"; import { client } from "../CosmosClient"; -import { readDatabases } from "./readDatabases"; +import { readDatabases, readDatabasesWithARM } from "./readDatabases"; describe("readDatabases", () => { beforeAll(() => { @@ -42,3 +43,149 @@ describe("readDatabases", () => { expect(client).toHaveBeenCalled(); }); }); + +describe("readDatabasesWithARM (with accountOverride)", () => { + const mockDatabase = { id: "testDb", _rid: "", _self: "", _etag: "", _ts: 0 }; + const mockArmResponse = { value: [{ properties: { resource: mockDatabase } }] }; + + beforeAll(() => { + updateUserContext({ + databaseAccount: { name: "context-account" } as DatabaseAccount, + subscriptionId: "context-sub", + resourceGroup: "context-rg", + apiType: "SQL", + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should call ARM with a path that includes the provided subscriptionId, resourceGroup, and accountName", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ subscriptionId: "test-sub", resourceGroup: "test-rg", accountName: "test-account" }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: expect.stringContaining("/subscriptions/test-sub/resourceGroups/test-rg/"), + }), + ); + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: expect.stringContaining("/databaseAccounts/test-account/sqlDatabases"), + }), + ); + }); + + it("should use apiType from accountOverride when provided (SQL)", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account", apiType: "SQL" }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/sqlDatabases") }), + ); + }); + + it("should use apiType from accountOverride when provided (Mongo)", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ + subscriptionId: "sub", + resourceGroup: "rg", + accountName: "account", + apiType: "Mongo", + }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/mongodbDatabases") }), + ); + }); + + it("should use apiType from accountOverride when provided (Cassandra)", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ + subscriptionId: "sub", + resourceGroup: "rg", + accountName: "account", + apiType: "Cassandra", + }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/cassandraKeyspaces") }), + ); + }); + + it("should use apiType from accountOverride when provided (Gremlin)", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ + subscriptionId: "sub", + resourceGroup: "rg", + accountName: "account", + apiType: "Gremlin", + }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/gremlinDatabases") }), + ); + }); + + it("should fall back to userContext.apiType when apiType is not in accountOverride", async () => { + updateUserContext({ apiType: "Mongo" }); + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/mongodbDatabases") }), + ); + + updateUserContext({ apiType: "SQL" }); // restore + }); + + it("should throw for unsupported apiType", async () => { + await expect( + readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account", apiType: "Tables" }), + ).rejects.toThrow("Unsupported default experience type: Tables"); + }); + + it("should return mapped database resources from the response", async () => { + const db1 = { id: "db1", _rid: "r1", _self: "/dbs/db1", _etag: "", _ts: 1 }; + const db2 = { id: "db2", _rid: "r2", _self: "/dbs/db2", _etag: "", _ts: 2 }; + + (armRequest as jest.Mock).mockResolvedValue({ + value: [{ properties: { resource: db1 } }, { properties: { resource: db2 } }], + }); + + const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }); + + expect(result).toEqual([db1, db2]); + }); + + it("should return an empty array when the response is null", async () => { + (armRequest as jest.Mock).mockResolvedValue(null); + + const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }); + + expect(result).toEqual([]); + }); + + it("should return an empty array when value is an empty list", async () => { + (armRequest as jest.Mock).mockResolvedValue({ value: [] }); + + const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }); + + expect(result).toEqual([]); + }); + + it("should throw and propagate errors from the ARM call", async () => { + (armRequest as jest.Mock).mockRejectedValue(new Error("ARM request failed")); + + await expect( + readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }), + ).rejects.toThrow("ARM request failed"); + }); +}); diff --git a/src/Common/dataAccess/readDatabases.ts b/src/Common/dataAccess/readDatabases.ts index 85b1c3186..e9f43e65f 100644 --- a/src/Common/dataAccess/readDatabases.ts +++ b/src/Common/dataAccess/readDatabases.ts @@ -4,7 +4,7 @@ import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; -import { FabricArtifactInfo, userContext } from "../../UserContext"; +import { ApiType, FabricArtifactInfo, userContext } from "../../UserContext"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; @@ -96,10 +96,17 @@ export async function readDatabases(): Promise { return databases; } -async function readDatabasesWithARM(): Promise { +export async function readDatabasesWithARM(accountOverride?: { + subscriptionId: string; + resourceGroup: string; + accountName: string; + apiType?: ApiType; +}): Promise { let rpResponse; - const { subscriptionId, resourceGroup, apiType, databaseAccount } = userContext; - const accountName = databaseAccount.name; + const subscriptionId = accountOverride?.subscriptionId ?? userContext.subscriptionId ?? ""; + const resourceGroup = accountOverride?.resourceGroup ?? userContext.resourceGroup ?? ""; + const accountName = accountOverride?.accountName ?? userContext?.databaseAccount?.name ?? ""; + const apiType = accountOverride?.apiType ?? userContext.apiType; switch (apiType) { case "SQL": @@ -118,5 +125,5 @@ async function readDatabasesWithARM(): Promise { throw new Error(`Unsupported default experience type: ${apiType}`); } - return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database); + return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? []; } diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index a72ac434e..770ee7948 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -406,11 +406,22 @@ export interface AutoPilotOfferSettings { targetMaxThroughput?: number; } +export interface AccountOverride { + subscriptionId: string; + resourceGroup: string; + accountName: string; + capabilities: Capability[]; + capacityMode?: CapacityMode; + enableFreeTier?: boolean; + enableAnalyticalStorage?: boolean; +} + export interface CreateDatabaseParams { autoPilotMaxThroughput?: number; databaseId: string; databaseLevelThroughput?: boolean; offerThroughput?: number; + targetAccountOverride?: AccountOverride; } export interface CreateCollectionParamsBase { @@ -430,6 +441,7 @@ export interface CreateCollectionParamsBase { export interface CreateCollectionParams extends CreateCollectionParamsBase { createNewDatabase: boolean; collectionId: string; + targetAccountOverride?: AccountOverride; } export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase { diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx index 9bb7fe3ea..b491f6bf5 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx @@ -457,13 +457,13 @@ describe("CopyJobActions", () => { jobName: "test-job", migrationType: "online" as any, source: { - subscription: {} as any, + subscriptionId: "sub-123", account: { id: "account-1", name: "source-account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "sub-123", + subscription: {} as any, account: { id: "account-1", name: "target-account" } as any, databaseId: "target-db", containerId: "target-container", @@ -498,7 +498,7 @@ describe("CopyJobActions", () => { ); const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4]; - expect(callArgs.properties.source.remoteAccountName).toBeUndefined(); + expect(callArgs.properties.destination.remoteAccountName).toBeUndefined(); expect(mockRefreshJobList).toHaveBeenCalled(); expect(mockOnSuccess).toHaveBeenCalled(); @@ -509,13 +509,13 @@ describe("CopyJobActions", () => { jobName: "cross-account-job", migrationType: "offline" as any, source: { - subscription: {} as any, + subscriptionId: "sub-123", account: { id: "account-1", name: "source-account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "sub-456", + subscription: {} as any, account: { id: "account-2", name: "target-account" } as any, databaseId: "target-db", containerId: "target-container", @@ -528,7 +528,7 @@ describe("CopyJobActions", () => { await submitCreateCopyJob(mockState, mockOnSuccess); const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4]; - expect(callArgs.properties.source.remoteAccountName).toBe("source-account"); + expect(callArgs.properties.destination.remoteAccountName).toBe("target-account"); expect(mockOnSuccess).toHaveBeenCalled(); }); @@ -537,13 +537,13 @@ describe("CopyJobActions", () => { jobName: "failing-job", migrationType: "online" as any, source: { - subscription: {} as any, + subscriptionId: "sub-123", account: { id: "account-1", name: "source-account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "sub-123", + subscription: {} as any, account: { id: "account-1", name: "target-account" } as any, databaseId: "target-db", containerId: "target-container", @@ -566,13 +566,13 @@ describe("CopyJobActions", () => { jobName: "test-job", migrationType: "online" as any, source: { - subscription: {} as any, + subscriptionId: "sub-123", account: { id: "account-1", name: "source-account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "sub-123", + subscription: {} as any, account: { id: "account-1", name: "target-account" } as any, databaseId: "target-db", containerId: "target-container", diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx index 821f87bc9..359243d10 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx @@ -1,4 +1,5 @@ import Explorer from "Explorer/Explorer"; +import { Keys, t } from "Localization"; import React from "react"; import { userContext } from "UserContext"; import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers"; @@ -15,7 +16,6 @@ import { CreateJobRequest, DataTransferJobGetResults, } from "../../../Utils/arm/generatedClients/dataTransferService/types"; -import ContainerCopyMessages from "../ContainerCopyMessages"; import { convertTime, convertToCamelCase, @@ -35,7 +35,7 @@ export const openCreateCopyJobPanel = (explorer: Explorer) => { const sidePanelState = useSidePanel.getState(); sidePanelState.setPanelHasConsole(false); sidePanelState.openSidePanel( - ContainerCopyMessages.createCopyJobPanelTitle, + t(Keys.containerCopy.createCopyJob.panelTitle), , "650px", ); @@ -45,7 +45,7 @@ export const openCopyJobDetailsPanel = (job: CopyJobType) => { const sidePanelState = useSidePanel.getState(); sidePanelState.setPanelHasConsole(false); sidePanelState.openSidePanel( - ContainerCopyMessages.copyJobDetailsPanelTitle(job.Name), + job.Name || t(Keys.containerCopy.jobDetails.panelTitleDefault), , "650px", ); @@ -137,12 +137,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess: properties: { source: { component: "CosmosDBSql", - ...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }), databaseName: source?.databaseId, containerName: source?.containerId, }, destination: { component: "CosmosDBSql", + ...(isSameAccount ? {} : { remoteAccountName: target?.account?.name }), databaseName: target?.databaseId, containerName: target?.containerId, }, @@ -193,7 +193,7 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro const pattern = new RegExp(`'(${statusList.join("|")})'`, "g"); const normalizedErrorMessage = errorMessage.replace( pattern, - `'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`, + `'${t(Keys.containerCopy.monitorJobs.status.inProgress)}'`, ); logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus"); throw error; diff --git a/src/Explorer/ContainerCopy/CommandBar/Utils.ts b/src/Explorer/ContainerCopy/CommandBar/Utils.ts index 5b96a6c37..8296e0add 100644 --- a/src/Explorer/ContainerCopy/CommandBar/Utils.ts +++ b/src/Explorer/ContainerCopy/CommandBar/Utils.ts @@ -5,10 +5,10 @@ import RefreshIcon from "../../../../images/refresh-cosmos.svg"; import SunIcon from "../../../../images/SunIcon.svg"; import { configContext, Platform } from "../../../ConfigContext"; import { useThemeStore } from "../../../hooks/useTheme"; +import { Keys, t } from "../../../Localization"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; import * as Actions from "../Actions/CopyJobActions"; -import ContainerCopyMessages from "../ContainerCopyMessages"; import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState"; import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes"; @@ -19,15 +19,15 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand { key: "createCopyJob", iconSrc: AddIcon, - label: ContainerCopyMessages.createCopyJobButtonLabel, - ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel, + label: t(Keys.containerCopy.commandBar.createCopyJobButtonLabel), + ariaLabel: t(Keys.containerCopy.commandBar.createCopyJobButtonAriaLabel), onClick: () => Actions.openCreateCopyJobPanel(explorer), }, { key: "refresh", iconSrc: RefreshIcon, - label: ContainerCopyMessages.refreshButtonLabel, - ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel, + label: t(Keys.common.refresh), + ariaLabel: t(Keys.containerCopy.commandBar.refreshButtonAriaLabel), onClick: () => monitorCopyJobsRef?.refreshJobList(), }, { @@ -48,8 +48,8 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand buttons.push({ key: "feedback", iconSrc: FeedbackIcon, - label: ContainerCopyMessages.feedbackButtonLabel, - ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel, + label: t(Keys.containerCopy.commandBar.feedbackButtonLabel), + ariaLabel: t(Keys.containerCopy.commandBar.feedbackButtonAriaLabel), onClick: () => { explorer.openContainerCopyFeedbackBlade(); }, diff --git a/src/Explorer/ContainerCopy/ContainerCopyMessages.ts b/src/Explorer/ContainerCopy/ContainerCopyMessages.ts deleted file mode 100644 index 65b308b9b..000000000 --- a/src/Explorer/ContainerCopy/ContainerCopyMessages.ts +++ /dev/null @@ -1,193 +0,0 @@ -export default { - // Copy Job Command Bar - feedbackButtonLabel: "Feedback", - feedbackButtonAriaLabel: "Provide feedback on copy jobs", - refreshButtonLabel: "Refresh", - refreshButtonAriaLabel: "Refresh copy jobs", - createCopyJobButtonLabel: "Create Copy Job", - createCopyJobButtonAriaLabel: "Create a new container copy job", - - // No Copy Jobs Found - noCopyJobsTitle: "No copy jobs to show", - createCopyJobButtonText: "Create a container copy job", - - // Copy Job Details - copyJobDetailsPanelTitle: (jobName: string) => jobName || "Job Details", - errorTitle: "Error Details", - selectedContainers: "Selected Containers", - - // Create Copy Job Panel - createCopyJobPanelTitle: "Create copy job", - - // Select Account Screen - selectAccountDescription: "Please select a source account from which to copy.", - subscriptionDropdownLabel: "Subscription", - subscriptionDropdownPlaceholder: "Select a subscription", - sourceAccountDropdownLabel: "Account", - sourceAccountDropdownPlaceholder: "Select an account", - migrationTypeOptions: { - offline: { - title: "Offline mode", - description: - "Offline container copy jobs let you copy data from a source container to a destination Cosmos DB container for supported APIs. To ensure data integrity between the source and destination, we recommend stopping updates on the source container before creating the copy job. Learn more about [offline copy jobs](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql).", - }, - online: { - title: "Online mode", - description: - "Online container copy jobs let you copy data from a source container to a destination Cosmos DB NoSQL API container using the [All Versions and Delete](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) change feed. This allows updates to continue on the source while data is copied. A brief downtime is required at the end to safely switch over client applications to the destination container. Learn more about [online copy jobs](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started).", - }, - }, - - // Select Source and Target Containers Screen - selectSourceAndTargetContainersDescription: - "Please select a source container and a destination container to copy to.", - sourceContainerSubHeading: "Source container", - targetContainerSubHeading: "Destination container", - databaseDropdownLabel: "Database", - databaseDropdownPlaceholder: "Select a database", - containerDropdownLabel: "Container", - containerDropdownPlaceholder: "Select a container", - createNewContainerSubHeading: "Select the properties for your container.", - createContainerButtonLabel: "Create a new container", - createContainerHeading: "Create new container", - - // Preview and Create Screen - jobNameLabel: "Job name", - sourceSubscriptionLabel: "Source subscription", - sourceAccountLabel: "Source account", - sourceDatabaseLabel: "Source database", - sourceContainerLabel: "Source container", - targetDatabaseLabel: "Destination database", - targetContainerLabel: "Destination container", - - // Assign Permissions Screen - assignPermissions: { - crossAccountDescription: - "To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.", - intraAccountOnlineDescription: (accountName: string) => - `Follow the steps below to enable online copy on your "${accountName}" account.`, - crossAccountConfiguration: { - title: "Cross-account container copy", - description: (sourceAccount: string, destinationAccount: string) => - `Please follow the instruction below to grant requisite permissions to copy data from "${sourceAccount}" to "${destinationAccount}".`, - }, - onlineConfiguration: { - title: "Online container copy", - description: (accountName: string) => - `Please follow the instructions below to enable online copy on your "${accountName}" account.`, - }, - }, - toggleBtn: { - onText: "On", - offText: "Off", - }, - popoverOverlaySpinnerLabel: "Please wait while we process your request...", - addManagedIdentity: { - title: "System-assigned managed identity enabled.", - description: - "A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code.", - descriptionHrefText: "Learn more about Managed identities.", - descriptionHref: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview", - toggleLabel: "System assigned managed identity", - tooltip: { - content: "Learn more about", - hrefText: "Managed Identities.", - href: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview", - }, - userAssignedIdentityTooltip: "You can select an existing user assigned identity or create a new one.", - userAssignedIdentityLabel: "You may also select a user assigned managed identity.", - createUserAssignedIdentityLink: "Create User Assigned Managed Identity", - enablementTitle: "Enable system assigned managed identity", - enablementDescription: (accountName: string) => - accountName - ? `Enable system-assigned managed identity on the ${accountName}. To confirm, click the "Yes" button.` - : "", - }, - defaultManagedIdentity: { - title: "System-assigned managed identity set as default.", - description: (accountName: string) => - `Set the system-assigned managed identity as default for "${accountName}" by switching it on.`, - tooltip: { - content: "Learn more about", - hrefText: "Default Managed Identities.", - href: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview", - }, - popoverTitle: "System assigned managed identity set as default", - popoverDescription: (accountName: string) => - `Assign the system-assigned managed identity as the default for "${accountName}". To confirm, click the "Yes" button. `, - }, - readPermissionAssigned: { - title: "Read permissions assigned to the default identity.", - description: - "To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.", - tooltip: { - content: "Learn more about", - hrefText: "Read permissions.", - href: "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control", - }, - popoverTitle: "Read permissions assigned to default identity.", - popoverDescription: - "Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button.", - }, - pointInTimeRestore: { - title: "Point In Time Restore enabled", - description: (accessName: string) => - `To facilitate online container copy jobs, please update your "${accessName}" backup policy from periodic to continuous backup. Enabling continuous backup is required for this functionality.`, - tooltip: { - content: "Learn more about", - hrefText: "Continuous Backup", - href: "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction", - }, - buttonText: "Enable Point In Time Restore", - }, - onlineCopyEnabled: { - title: "Online copy enabled", - description: (accountName: string) => - `Enable online container copy by clicking the button below on your "${accountName}" account.`, - hrefText: "Learn more about online copy jobs", - href: "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy", - buttonText: "Enable Online Copy", - validateAllVersionsAndDeletesChangeFeedSpinnerLabel: - "Validating All versions and deletes change feed mode (preview)...", - enablingAllVersionsAndDeletesChangeFeedSpinnerLabel: - "Enabling All versions and deletes change feed mode (preview)...", - enablingOnlineCopySpinnerLabel: (accountName: string) => - `Enabling online copy on your "${accountName}" account ...`, - }, - MonitorJobs: { - Columns: { - lastUpdatedTime: "Date & time", - name: "Job name", - status: "Status", - completionPercentage: "Completion %", - duration: "Duration", - error: "Error message", - mode: "Mode", - actions: "Actions", - }, - Actions: { - pause: "Pause", - resume: "Resume", - cancel: "Cancel", - complete: "Complete", - viewDetails: "View Details", - }, - Status: { - Pending: "Queued", - InProgress: "Running", - Running: "Running", - Partitioning: "Running", - Paused: "Paused", - Completed: "Completed", - Failed: "Failed", - Faulted: "Failed", - Skipped: "Cancelled", - Cancelled: "Cancelled", - }, - dialog: { - heading: "", - confirmButtonText: "Confirm", - cancelButtonText: "Cancel", - }, - }, -}; diff --git a/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx b/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx index 7a0e7d874..f263f3cb3 100644 --- a/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx +++ b/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx @@ -59,12 +59,6 @@ describe("CopyJobContext", () => { jobName: "", migrationType: CopyJobMigrationType.Offline, source: { - subscription: null, - account: null, - databaseId: "", - containerId: "", - }, - target: { subscriptionId: "test-subscription-id", account: { id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account", @@ -75,7 +69,13 @@ describe("CopyJobContext", () => { databaseId: "", containerId: "", }, - sourceReadAccessFromTarget: false, + target: { + subscription: null, + account: null, + databaseId: "", + containerId: "", + }, + sourceReadWriteAccessFromTarget: false, }); expect(contextValue.flow).toBeNull(); expect(contextValue.contextError).toBeNull(); @@ -598,8 +598,8 @@ describe("CopyJobContext", () => { , ); - expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined(); - expect(contextValue.copyJobState.source?.account?.name).toBeUndefined(); + expect(contextValue.copyJobState.source?.subscriptionId).toBe("test-subscription-id"); + expect(contextValue.copyJobState.source?.account?.name).toBe("test-account"); }); it("should initialize target with userContext values", () => { @@ -616,11 +616,11 @@ describe("CopyJobContext", () => { , ); - expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id"); - expect(contextValue.copyJobState.target.account.name).toBe("test-account"); + expect(contextValue.copyJobState.target.subscription).toBeNull(); + expect(contextValue.copyJobState.target.account).toBeNull(); }); - it("should initialize sourceReadAccessFromTarget as false", () => { + it("should initialize sourceReadWriteAccessFromTarget as false", () => { let contextValue: any; render( @@ -634,7 +634,7 @@ describe("CopyJobContext", () => { , ); - expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false); + expect(contextValue.copyJobState.sourceReadWriteAccessFromTarget).toBe(false); }); it("should initialize with empty database and container ids", () => { diff --git a/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx b/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx index ddb936dcf..912ed0311 100644 --- a/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx +++ b/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx @@ -23,18 +23,18 @@ const getInitialCopyJobState = (): CopyJobContextState => { jobName: "", migrationType: CopyJobMigrationType.Offline, source: { - subscription: null, - account: null, - databaseId: "", - containerId: "", - }, - target: { subscriptionId: userContext.subscriptionId || "", account: userContext.databaseAccount || null, databaseId: "", containerId: "", }, - sourceReadAccessFromTarget: false, + target: { + subscription: null, + account: null, + databaseId: "", + containerId: "", + }, + sourceReadWriteAccessFromTarget: false, }; }; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.test.tsx index 6022b98d4..0e35c245c 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.test.tsx @@ -2,9 +2,9 @@ import "@testing-library/jest-dom"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { DatabaseAccount } from "Contracts/DataModels"; import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes"; +import { Keys, t } from "Localization"; import React from "react"; import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { CopyJobContext } from "../../../Context/CopyJobContext"; import AddManagedIdentity from "./AddManagedIdentity"; @@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => { databaseId: "target-db", containerId: "target-container", }, - sourceReadAccessFromTarget: false, + sourceReadWriteAccessFromTarget: false, }; const mockContextValue = { @@ -133,16 +133,16 @@ describe("AddManagedIdentity", () => { it("renders all required elements", () => { renderWithContext(); - expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.description)).toBeInTheDocument(); - expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText)).toBeInTheDocument(); + expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.description))).toBeInTheDocument(); + expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.descriptionHrefText))).toBeInTheDocument(); expect(screen.getByRole("switch")).toBeInTheDocument(); }); it("renders description link with correct href", () => { renderWithContext(); - const link = screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText); - expect(link.closest("a")).toHaveAttribute("href", ContainerCopyMessages.addManagedIdentity.descriptionHref); + const link = screen.getByText(t(Keys.containerCopy.addManagedIdentity.descriptionHrefText)); + expect(link.closest("a")).toHaveAttribute("href", t(Keys.containerCopy.addManagedIdentity.descriptionHref)); expect(link.closest("a")).toHaveAttribute("target", "_blank"); expect(link.closest("a")).toHaveAttribute("rel", "noopener noreferrer"); }); @@ -175,7 +175,7 @@ describe("AddManagedIdentity", () => { const toggle = screen.getByRole("switch"); fireEvent.click(toggle); - expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).toBeInTheDocument(); + expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.enablementTitle))).toBeInTheDocument(); }); it("hides popover when toggle is off", () => { @@ -185,7 +185,7 @@ describe("AddManagedIdentity", () => { fireEvent.click(toggle); fireEvent.click(toggle); - expect(screen.queryByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).not.toBeInTheDocument(); + expect(screen.queryByText(t(Keys.containerCopy.addManagedIdentity.enablementTitle))).not.toBeInTheDocument(); }); }); @@ -197,9 +197,9 @@ describe("AddManagedIdentity", () => { }); it("displays correct enablement description with account name", () => { - const expectedDescription = ContainerCopyMessages.addManagedIdentity.enablementDescription( - mockCopyJobState.target.account.name, - ); + const expectedDescription = t(Keys.containerCopy.addManagedIdentity.enablementDescription, { + accountName: mockCopyJobState.source.account.name, + }); expect(screen.getByText(expectedDescription)).toBeInTheDocument(); }); @@ -220,7 +220,7 @@ describe("AddManagedIdentity", () => { const cancelButton = screen.getByText("Cancel"); fireEvent.click(cancelButton); - expect(screen.queryByText(ContainerCopyMessages.addManagedIdentity.enablementTitle)).not.toBeInTheDocument(); + expect(screen.queryByText(t(Keys.containerCopy.addManagedIdentity.enablementTitle))).not.toBeInTheDocument(); const toggle = screen.getByRole("switch"); expect(toggle).not.toBeChecked(); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx index 86c59611c..fbc7baec0 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx @@ -1,7 +1,7 @@ import { Link, Stack, Text, Toggle } from "@fluentui/react"; +import { Keys, t } from "Localization"; import React from "react"; import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import InfoTooltip from "../Components/InfoTooltip"; import PopoverMessage from "../Components/PopoverContainer"; @@ -11,14 +11,14 @@ import useToggle from "./hooks/useToggle"; const managedIdentityTooltip = ( - {ContainerCopyMessages.addManagedIdentity.tooltip.content}   + {t(Keys.containerCopy.addManagedIdentity.tooltipContent)}   - {ContainerCopyMessages.addManagedIdentity.tooltip.hrefText} + {t(Keys.containerCopy.addManagedIdentity.tooltipHrefText)} ); @@ -32,9 +32,9 @@ const AddManagedIdentity: React.FC = () => { return ( - {ContainerCopyMessages.addManagedIdentity.description}  - - {ContainerCopyMessages.addManagedIdentity.descriptionHrefText} + {t(Keys.containerCopy.addManagedIdentity.description)}  + + {t(Keys.containerCopy.addManagedIdentity.descriptionHrefText)} {" "}   @@ -42,18 +42,20 @@ const AddManagedIdentity: React.FC = () => { onToggle(null, false)} onPrimary={handleAddSystemIdentity} > - {ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)} + {t(Keys.containerCopy.addManagedIdentity.enablementDescription, { + accountName: copyJobState.source?.account?.name, + })} ); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx index 5ef3577b8..927840b86 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx @@ -1,10 +1,10 @@ import "@testing-library/jest-dom"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import React from "react"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; +import { Keys, t } from "Localization"; import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes"; -import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity"; +import AddReadWritePermissionToDefaultIdentity from "./AddReadWritePermissionToDefaultIdentity"; jest.mock("../../../../../Common/Logger", () => ({ logError: jest.fn(), @@ -73,7 +73,7 @@ import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUti import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import useToggle from "./hooks/useToggle"; -describe("AddReadPermissionToDefaultIdentity Component", () => { +describe("AddReadWritePermissionToDefaultIdentity Component", () => { const mockUseToggle = useToggle as jest.MockedFunction; const mockAssignRole = assignRole as jest.MockedFunction; const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction< @@ -86,7 +86,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { jobName: "test-job", migrationType: CopyJobMigrationType.Offline, source: { - subscription: { subscriptionId: "source-sub-id" } as Subscription, + subscriptionId: "source-sub-id", account: { id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account", name: "source-account", @@ -96,12 +96,16 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { properties: { documentEndpoint: "https://source-account.documents.azure.com:443/", }, + identity: { + principalId: "source-principal-id", + type: "SystemAssigned", + }, }, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "target-sub-id", + subscription: { subscriptionId: "target-sub-id" } as Subscription, account: { id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account", name: "target-account", @@ -119,7 +123,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { databaseId: "target-db", containerId: "target-container", }, - sourceReadAccessFromTarget: false, + sourceReadWriteAccessFromTarget: false, }, setCopyJobState: jest.fn(), setContextError: jest.fn(), @@ -133,7 +137,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { const renderComponent = (contextValue = mockContextValue) => { return render( - + , ); }; @@ -164,12 +168,12 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { expect(container).toMatchSnapshot(); }); - it("should render correctly when sourceReadAccessFromTarget is true", () => { + it("should render correctly when sourceReadWriteAccessFromTarget is true", () => { const contextWithAccess = { ...mockContextValue, copyJobState: { ...mockContextValue.copyJobState, - sourceReadAccessFromTarget: true, + sourceReadWriteAccessFromTarget: true, }, }; const { container } = renderComponent(contextWithAccess); @@ -180,7 +184,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { describe("Component Structure", () => { it("should display the description text", () => { renderComponent(); - expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument(); + expect(screen.getByText(t(Keys.containerCopy.readWritePermissionAssigned.description))).toBeInTheDocument(); }); it("should display the info tooltip", () => { @@ -212,10 +216,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { expect(screen.getByTestId("popover-message")).toBeInTheDocument(); expect(screen.getByTestId("popover-title")).toHaveTextContent( - ContainerCopyMessages.readPermissionAssigned.popoverTitle, + t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle), ); expect(screen.getByTestId("popover-content")).toHaveTextContent( - ContainerCopyMessages.readPermissionAssigned.popoverDescription, + t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription), ); }); @@ -243,11 +247,11 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { expect(mockOnToggle).toHaveBeenCalledWith(null, false); }); - it("should call handleAddReadPermission when primary button is clicked", async () => { + it("should call handleAddReadWritePermission when primary button is clicked", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); @@ -258,22 +262,22 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { await waitFor(() => { expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith( - "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account", + "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account", ); }); }); }); - describe("handleAddReadPermission Function", () => { + describe("handleAddReadWritePermission Function", () => { beforeEach(() => { mockUseToggle.mockReturnValue([true, jest.fn()]); }); it("should successfully assign role and update context", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); @@ -284,10 +288,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { await waitFor(() => { expect(mockAssignRole).toHaveBeenCalledWith( - "source-sub-id", - "source-rg", - "source-account", - "target-principal-id", + "target-sub-id", + "target-rg", + "target-account", + "source-principal-id", ); }); @@ -298,9 +302,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { it("should handle error when assignRole fails", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockRejectedValue(new Error("Permission denied")); @@ -312,7 +316,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { await waitFor(() => { expect(mockLogError).toHaveBeenCalledWith( "Permission denied", - "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission", + "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission", ); }); @@ -323,9 +327,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { it("should handle error without message", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockRejectedValue({}); @@ -336,23 +340,23 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { await waitFor(() => { expect(mockLogError).toHaveBeenCalledWith( - "Error assigning read permission to default identity. Please try again later.", - "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission", + "Error assigning read-write permission to default identity. Please try again later.", + "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission", ); }); await waitFor(() => { expect(mockContextValue.setContextError).toHaveBeenCalledWith( - "Error assigning read permission to default identity. Please try again later.", + "Error assigning read-write permission to default identity. Please try again later.", ); }); }); it("should show loading state during role assignment", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockImplementation( @@ -371,9 +375,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { it.skip("should not assign role when assignRole returns falsy", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue(null); @@ -431,10 +435,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { ...mockContextValue, copyJobState: { ...mockContextValue.copyJobState, - target: { - ...mockContextValue.copyJobState.target, + source: { + ...mockContextValue.copyJobState.source, account: { - ...mockContextValue.copyJobState.target.account!, + ...mockContextValue.copyJobState.source.account!, identity: { principalId: "", type: "SystemAssigned", @@ -446,9 +450,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { mockUseToggle.mockReturnValue([true, jest.fn()]); mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); @@ -458,7 +462,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { fireEvent.click(primaryButton); await waitFor(() => { - expect(mockAssignRole).toHaveBeenCalledWith("source-sub-id", "source-rg", "source-account", ""); + expect(mockAssignRole).toHaveBeenCalledWith("target-sub-id", "target-rg", "target-account", ""); }); }); }); @@ -476,9 +480,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { mockUseToggle.mockReturnValue([true, jest.fn()]); mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); @@ -496,7 +500,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { expect(updatedState).toEqual({ ...mockContextValue.copyJobState, - sourceReadAccessFromTarget: true, + sourceReadWriteAccessFromTarget: true, }); }); }); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadWritePermissionToDefaultIdentity.tsx similarity index 51% rename from src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx rename to src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadWritePermissionToDefaultIdentity.tsx index 5af5630d7..d042d15a7 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadWritePermissionToDefaultIdentity.tsx @@ -1,8 +1,8 @@ import { Link, Stack, Text, Toggle } from "@fluentui/react"; +import { Keys, t } from "Localization"; import React from "react"; import { logError } from "../../../../../Common/Logger"; import { assignRole } from "../../../../../Utils/arm/RbacUtils"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import InfoTooltip from "../Components/InfoTooltip"; @@ -12,51 +12,54 @@ import useToggle from "./hooks/useToggle"; const TooltipContent = ( - {ContainerCopyMessages.readPermissionAssigned.tooltip.content}   + {t(Keys.containerCopy.readWritePermissionAssigned.tooltipContent)}   - {ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText} + {t(Keys.containerCopy.readWritePermissionAssigned.tooltipHrefText)} ); -type AddReadPermissionToDefaultIdentityProps = Partial; -const AddReadPermissionToDefaultIdentity: React.FC = () => { +type AddReadWritePermissionToDefaultIdentityProps = Partial; + +const AddReadWritePermissionToDefaultIdentity: React.FC = () => { const [loading, setLoading] = React.useState(false); const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext(); - const [readPermissionAssigned, onToggle] = useToggle(false); + const [readWritePermissionAssigned, onToggle] = useToggle(copyJobState.sourceReadWriteAccessFromTarget ?? false); - const handleAddReadPermission = async () => { + const handleAddReadWritePermission = async () => { const { source, target } = copyJobState; - const selectedSourceAccount = source?.account; + const selectedTargetAccount = target?.account; + try { const { - subscriptionId: sourceSubscriptionId, - resourceGroup: sourceResourceGroup, - accountName: sourceAccountName, - } = getAccountDetailsFromResourceId(selectedSourceAccount?.id); + subscriptionId: targetSubscriptionId, + resourceGroup: targetResourceGroup, + accountName: targetAccountName, + } = getAccountDetailsFromResourceId(selectedTargetAccount?.id); setLoading(true); const assignedRole = await assignRole( - sourceSubscriptionId, - sourceResourceGroup, - sourceAccountName, - target?.account?.identity?.principalId ?? "", + targetSubscriptionId, + targetResourceGroup, + targetAccountName, + source?.account?.identity?.principalId ?? "", ); + if (assignedRole) { setCopyJobState((prevState) => ({ ...prevState, - sourceReadAccessFromTarget: true, + sourceReadWriteAccessFromTarget: true, })); } } catch (error) { const errorMessage = - error.message || "Error assigning read permission to default identity. Please try again later."; - logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission"); + error.message || "Error assigning read-write permission to default identity. Please try again later."; + logError(errorMessage, "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission"); setContextError(errorMessage); } finally { setLoading(false); @@ -66,14 +69,14 @@ const AddReadPermissionToDefaultIdentity: React.FC - {ContainerCopyMessages.readPermissionAssigned.description}  + {t(Keys.containerCopy.readWritePermissionAssigned.description)}  onToggle(null, false)} - onPrimary={handleAddReadPermission} + onPrimary={handleAddReadWritePermission} > - {ContainerCopyMessages.readPermissionAssigned.popoverDescription} + {t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription)} ); }; -export default AddReadPermissionToDefaultIdentity; +export default AddReadWritePermissionToDefaultIdentity; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx index 76f915c5e..4c35e306f 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx @@ -1,7 +1,7 @@ import "@testing-library/jest-dom"; import { render, RenderResult } from "@testing-library/react"; import React from "react"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; +import { Keys, t } from "Localization"; import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes"; @@ -43,12 +43,12 @@ jest.mock("./AddManagedIdentity", () => { return MockAddManagedIdentity; }); -jest.mock("./AddReadPermissionToDefaultIdentity", () => { - const MockAddReadPermissionToDefaultIdentity = () => { - return
Add Read Permission Component
; +jest.mock("./AddReadWritePermissionToDefaultIdentity", () => { + const MockAddReadWritePermissionToDefaultIdentity = () => { + return
Add Read-Write Permission Component
; }; - MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity"; - return MockAddReadPermissionToDefaultIdentity; + MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity"; + return MockAddReadWritePermissionToDefaultIdentity; }); jest.mock("./DefaultManagedIdentity", () => { @@ -85,18 +85,18 @@ describe("AssignPermissions Component", () => { jobName: "test-job", migrationType: CopyJobMigrationType.Offline, source: { - subscription: { subscriptionId: "source-sub" } as any, + subscriptionId: "source-sub", account: { id: "source-account", name: "Source Account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "target-sub", + subscription: { subscriptionId: "target-sub" } as any, account: { id: "target-account", name: "Target Account" } as any, databaseId: "target-db", containerId: "target-container", }, - sourceReadAccessFromTarget: false, + sourceReadWriteAccessFromTarget: false, ...overrides, }); @@ -154,7 +154,7 @@ describe("AssignPermissions Component", () => { const copyJobState = createMockCopyJobState(); const { getByText } = renderWithContext(copyJobState); - expect(getByText(ContainerCopyMessages.assignPermissions.crossAccountDescription)).toBeInTheDocument(); + expect(getByText(t(Keys.containerCopy.assignPermissions.crossAccountDescription))).toBeInTheDocument(); }); it("should display intra account description for same accounts with online migration", async () => { @@ -164,13 +164,13 @@ describe("AssignPermissions Component", () => { const copyJobState = createMockCopyJobState({ migrationType: CopyJobMigrationType.Online, source: { - subscription: { subscriptionId: "same-sub" } as any, + subscriptionId: "same-sub", account: { id: "same-account", name: "Same Account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "same-sub", + subscription: { subscriptionId: "same-sub" } as any, account: { id: "same-account", name: "Same Account" } as any, databaseId: "target-db", containerId: "target-container", @@ -179,7 +179,9 @@ describe("AssignPermissions Component", () => { const { getByText } = renderWithContext(copyJobState); expect( - getByText(ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription("Same Account")), + getByText( + t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, { accountName: "Same Account" }), + ), ).toBeInTheDocument(); }); }); @@ -201,7 +203,7 @@ describe("AssignPermissions Component", () => { completed: true, }, { - id: "readPermissionAssigned", + id: "readWritePermissionAssigned", title: "Read Permission Assigned", Component: () =>
Add Read Permission Component
, disabled: false, @@ -347,7 +349,7 @@ describe("AssignPermissions Component", () => { it("should handle missing account names", () => { const copyJobState = createMockCopyJobState({ source: { - subscription: { subscriptionId: "source-sub" } as any, + subscriptionId: "source-sub", account: { id: "source-account" } as any, databaseId: "source-db", containerId: "source-container", diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx index 40a657f59..43096e121 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx @@ -1,10 +1,10 @@ import { Image, Stack, Text } from "@fluentui/react"; import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components"; +import { Keys, t } from "Localization"; import React, { useEffect } from "react"; import CheckmarkIcon from "../../../../../../images/successfulPopup.svg"; import WarningIcon from "../../../../../../images/warning.svg"; import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { isIntraAccountCopy } from "../../../CopyJobUtils"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; @@ -106,11 +106,11 @@ const AssignPermissions = () => { tokens={{ childrenGap: 20 }} > - {isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online - ? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription( - copyJobState?.source?.account?.name || "", - ) - : ContainerCopyMessages.assignPermissions.crossAccountDescription} + {isSameAccount && copyJobState?.migrationType === CopyJobMigrationType.Online + ? t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, { + accountName: copyJobState?.source?.account?.name || "", + }) + : t(Keys.containerCopy.assignPermissions.crossAccountDescription)} {totalSectionsCount === 0 ? ( diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx index 93418859f..201e10bfa 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx @@ -1,8 +1,8 @@ import "@testing-library/jest-dom"; import { fireEvent, render, screen } from "@testing-library/react"; +import { Keys, t } from "Localization"; import React from "react"; import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { CopyJobContext } from "../../../Context/CopyJobContext"; import DefaultManagedIdentity from "./DefaultManagedIdentity"; @@ -69,6 +69,12 @@ const mockUseToggle = useToggle as jest.MockedFunction; describe("DefaultManagedIdentity", () => { const mockCopyJobContextValue = { copyJobState: { + source: { + account: { + name: "test-cosmos-account", + id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos-account", + }, + }, target: { account: { name: "test-cosmos-account", @@ -166,7 +172,7 @@ describe("DefaultManagedIdentity", () => { expect(popover).toBeInTheDocument(); const title = screen.getByTestId("popover-title"); - expect(title).toHaveTextContent(ContainerCopyMessages.defaultManagedIdentity.popoverTitle); + expect(title).toHaveTextContent(t(Keys.containerCopy.defaultManagedIdentity.popoverTitle)); const content = screen.getByTestId("popover-content"); expect(content).toHaveTextContent( @@ -260,6 +266,12 @@ describe("DefaultManagedIdentity", () => { const contextValueWithoutAccount = { ...mockCopyJobContextValue, copyJobState: { + source: { + account: { + name: "", + id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/", + }, + }, target: { account: { name: "", @@ -277,6 +289,9 @@ describe("DefaultManagedIdentity", () => { const contextValueWithNullAccount = { ...mockCopyJobContextValue, copyJobState: { + source: { + account: null as DatabaseAccount | null, + }, target: { account: null as DatabaseAccount | null, }, @@ -339,8 +354,8 @@ describe("DefaultManagedIdentity", () => { it("should display correct toggle button text", () => { renderComponent(); - const onText = screen.queryByText(ContainerCopyMessages.toggleBtn.onText); - const offText = screen.queryByText(ContainerCopyMessages.toggleBtn.offText); + const onText = screen.queryByText(t(Keys.common.on)); + const offText = screen.queryByText(t(Keys.common.off)); expect(onText || offText).toBeTruthy(); }); @@ -348,7 +363,7 @@ describe("DefaultManagedIdentity", () => { it("should display correct link text in tooltip", () => { renderComponent(); - const linkText = screen.getByText(ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText); + const linkText = screen.getByText(t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText)); expect(linkText).toBeInTheDocument(); }); }); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx index 92c752171..aa76220ee 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx @@ -1,7 +1,7 @@ import { Link, Stack, Text, Toggle } from "@fluentui/react"; +import { Keys, t } from "Localization"; import React from "react"; import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import InfoTooltip from "../Components/InfoTooltip"; import PopoverMessage from "../Components/PopoverContainer"; @@ -11,14 +11,14 @@ import useToggle from "./hooks/useToggle"; const managedIdentityTooltip = ( - {ContainerCopyMessages.defaultManagedIdentity.tooltip.content}   + {t(Keys.containerCopy.defaultManagedIdentity.tooltipContent)}   - {ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText} + {t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText)} ); @@ -32,14 +32,17 @@ const DefaultManagedIdentity: React.FC = () => { return (
- {ContainerCopyMessages.defaultManagedIdentity.description(copyJobState?.target?.account?.name)}   + {t(Keys.containerCopy.defaultManagedIdentity.description, { + accountName: copyJobState?.source?.account?.name, + })}{" "} +  
= () => { onToggle(null, false)} onPrimary={handleAddSystemIdentity} > - {ContainerCopyMessages.defaultManagedIdentity.popoverDescription(copyJobState?.target?.account?.name)} + {t(Keys.containerCopy.defaultManagedIdentity.popoverDescription, { + accountName: copyJobState?.source?.account?.name, + })}
); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx index 8d28f2482..5e877fcdd 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx @@ -2,12 +2,12 @@ import "@testing-library/jest-dom"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { DatabaseAccount } from "Contracts/DataModels"; import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes"; +import { Keys, t } from "Localization"; import React from "react"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { CapabilityNames } from "../../../../../Common/Constants"; import { logError } from "../../../../../Common/Logger"; import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { CopyJobContext } from "../../../Context/CopyJobContext"; import OnlineCopyEnabled from "./OnlineCopyEnabled"; @@ -97,7 +97,9 @@ describe("OnlineCopyEnabled", () => { it("should render the description with account name", () => { renderComponent(); - const description = screen.getByText(ContainerCopyMessages.onlineCopyEnabled.description("test-account")); + const description = screen.getByText( + t(Keys.containerCopy.onlineCopyEnabled.description, { accountName: "test-account" }), + ); expect(description).toBeInTheDocument(); }); @@ -105,10 +107,10 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const link = screen.getByRole("link", { - name: ContainerCopyMessages.onlineCopyEnabled.hrefText, + name: t(Keys.containerCopy.onlineCopyEnabled.hrefText), }); expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute("href", ContainerCopyMessages.onlineCopyEnabled.href); + expect(link).toHaveAttribute("href", t(Keys.containerCopy.onlineCopyEnabled.href)); expect(link).toHaveAttribute("target", "_blank"); expect(link).toHaveAttribute("rel", "noopener noreferrer"); }); @@ -117,7 +119,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const button = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); expect(button).toBeInTheDocument(); expect(button).not.toBeDisabled(); @@ -134,7 +136,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const refreshButton = screen.queryByRole("button", { - name: ContainerCopyMessages.refreshButtonLabel, + name: t(Keys.common.refresh), }); expect(refreshButton).not.toBeInTheDocument(); }); @@ -167,7 +169,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -222,7 +224,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -246,7 +248,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -259,7 +261,9 @@ describe("OnlineCopyEnabled", () => { await waitFor(() => { expect( - screen.getByText(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel("test-account")), + screen.getByText( + t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { accountName: "test-account" }), + ), ).toBeInTheDocument(); }); }); @@ -272,7 +276,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -306,7 +310,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -318,7 +322,7 @@ describe("OnlineCopyEnabled", () => { }); const refreshButton = screen.getByRole("button", { - name: ContainerCopyMessages.refreshButtonLabel, + name: t(Keys.common.refresh), }); await act(async () => { @@ -349,7 +353,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -379,7 +383,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -401,7 +405,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -418,7 +422,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -436,7 +440,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); await act(async () => { @@ -450,7 +454,7 @@ describe("OnlineCopyEnabled", () => { mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {})); const refreshButton = screen.getByRole("button", { - name: ContainerCopyMessages.refreshButtonLabel, + name: t(Keys.common.refresh), }); await act(async () => { @@ -536,7 +540,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(contextWithNoCapabilities); const enableButton = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); expect(enableButton).toBeInTheDocument(); }); @@ -547,7 +551,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const button = screen.getByRole("button", { - name: ContainerCopyMessages.onlineCopyEnabled.buttonText, + name: t(Keys.containerCopy.onlineCopyEnabled.buttonText), }); expect(button).toBeInTheDocument(); }); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx index a6f0918c3..ae6f6b728 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx @@ -1,12 +1,12 @@ import { Link, PrimaryButton, Stack } from "@fluentui/react"; import { DatabaseAccount } from "Contracts/DataModels"; +import { Keys, t } from "Localization"; import React from "react"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { CapabilityNames } from "../../../../../Common/Constants"; import LoadingOverlay from "../../../../../Common/LoadingOverlay"; import { logError } from "../../../../../Common/Logger"; import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { AccountValidatorFn } from "../../../Types/CopyJobTypes"; @@ -76,21 +76,25 @@ const OnlineCopyEnabled: React.FC = () => { setShowRefreshButton(false); try { - setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel); + setLoaderMessage(t(Keys.containerCopy.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel)); const sourAccountBeforeUpdate = await fetchDatabaseAccount( sourceSubscriptionId, sourceResourceGroup, sourceAccountName, ); if (!sourAccountBeforeUpdate?.properties.enableAllVersionsAndDeletesChangeFeed) { - setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel); + setLoaderMessage(t(Keys.containerCopy.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel)); await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { properties: { enableAllVersionsAndDeletesChangeFeed: true, }, }); } - setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel(sourceAccountName)); + setLoaderMessage( + t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { + accountName: sourceAccountName, + }), + ); await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { properties: { capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }], @@ -132,16 +136,16 @@ const OnlineCopyEnabled: React.FC = () => { - {ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")}  - - {ContainerCopyMessages.onlineCopyEnabled.hrefText} + {t(Keys.containerCopy.onlineCopyEnabled.description, { accountName: source?.account?.name || "" })}  + + {t(Keys.containerCopy.onlineCopyEnabled.hrefText)} {showRefreshButton ? ( { ) : ( { jobName: "test-job", migrationType: CopyJobMigrationType.Offline, source: { - subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" }, + subscriptionId: "test-sub", account: mockSourceAccount, databaseId: "test-db", containerId: "test-container", }, target: { - subscriptionId: "test-sub", + subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" }, account: mockSourceAccount, databaseId: "target-db", containerId: "target-container", }, - sourceReadAccessFromTarget: false, + sourceReadWriteAccessFromTarget: false, } as CopyJobContextState; const mockSetCopyJobState = jest.fn(); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.tsx index 6725f4981..25d6237a7 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.tsx @@ -1,10 +1,10 @@ import { Link, PrimaryButton, Stack, Text } from "@fluentui/react"; import { DatabaseAccount } from "Contracts/DataModels"; +import { Keys, t } from "Localization"; import React, { useEffect, useRef, useState } from "react"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import LoadingOverlay from "../../../../../Common/LoadingOverlay"; import { logError } from "../../../../../Common/Logger"; -import ContainerCopyMessages from "../../../ContainerCopyMessages"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { AccountValidatorFn } from "../../../Types/CopyJobTypes"; @@ -12,14 +12,14 @@ import InfoTooltip from "../Components/InfoTooltip"; const tooltipContent = ( - {ContainerCopyMessages.pointInTimeRestore.tooltip.content}   + {t(Keys.containerCopy.pointInTimeRestore.tooltipContent)}   - {ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText} + {t(Keys.containerCopy.pointInTimeRestore.tooltipHrefText)} ); @@ -119,9 +119,9 @@ const PointInTimeRestore: React.FC = () => { return ( - + - {ContainerCopyMessages.pointInTimeRestore.description(source.account?.name ?? "")} + {t(Keys.containerCopy.pointInTimeRestore.description, { accessName: source.account?.name ?? "" })} {tooltipContent && ( <> {" "} @@ -134,7 +134,7 @@ const PointInTimeRestore: React.FC = () => { @@ -142,7 +142,7 @@ const PointInTimeRestore: React.FC = () => { - A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code. + A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.   - A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code. + A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.   - Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button. + Enable system-assigned managed identity on the source-account-name. To confirm, click the "Yes" button.
- A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don’t have to store any credentials in code. + A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you don't have to store any credentials in code.   - Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button. + Enable system-assigned managed identity on the source-account-name. To confirm, click the "Yes" button.
`; -exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = ` +exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
- To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. + To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.  
- Read permissions. + Read-write permissions.
@@ -126,7 +126,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
`; -exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when sourceReadAccessFromTarget is true 1`] = ` +exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when sourceReadWriteAccessFromTarget is true 1`] = `
- To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. + To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.  
- Read permissions. + Read-write permissions.
@@ -189,7 +189,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
`; -exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = ` +exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
- To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. + To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.  
- Read permissions. + Read-write permissions.
@@ -255,12 +255,12 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
- Read permissions assigned to default identity. + Assign read-write permissions to default identity.
- Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button. + Assign read-write permissions on the destination account to the default identity of the source account. To confirm, click the "Yes" button.