Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/jawelton/remove-gallery

This commit is contained in:
Jade Welton
2026-05-21 07:58:30 -07:00
116 changed files with 6016 additions and 1649 deletions
+62
View File
@@ -314,6 +314,9 @@ jobs:
needs: [playwright-tests] needs: [playwright-tests]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@@ -338,3 +341,62 @@ jobs:
name: html-report--attempt-${{ github.run_attempt }} name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report path: playwright-report
retention-days: 14 retention-days: 14
- name: Comment Playwright results on PR
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
REPORT_URL: https://dataexplorerpreview.z5.web.core.windows.net/playwright-reports/${{ github.run_id }}-${{ github.run_attempt }}/index.html
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
PLAYWRIGHT_JSON_OUTPUT_NAME=results.json npx playwright merge-reports --reporter json ./all-blob-reports
read PASSED FAILED FLAKY SKIPPED DURATION < <(jq -r '[.stats.expected,.stats.unexpected,.stats.flaky,.stats.skipped,(.stats.duration/1000|floor)] | @tsv' results.json)
BROKEN=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}/jobs" \
--jq '[.jobs[] | select(.name | startswith("Run Playwright Tests")) | select(.conclusion == "failure")] | length')
if [ "$FAILED" -gt 0 ] || [ "$BROKEN" -gt 0 ]; then ICON="❌ failed"; else ICON="✅ passed"; fi
NOTE=""
[ "$BROKEN" -gt 0 ] && NOTE="
> ⚠️ $BROKEN shard(s) failed before tests ran (infra/auth issue). Stats below reflect only shards that executed."
gh pr comment "$PR" --body "### Playwright tests $ICON
| Passed | Failed | Flaky | Duration |
| :---: | :---: | :---: | :---: |
| $PASSED | $FAILED | $FLAKY | ${DURATION}s |
📊 [Open full report]($REPORT_URL) · [Workflow run]($RUN_URL)$NOTE"
publish-playwright-report:
name: "Publish Playwright Report to Blob"
if: ${{ !cancelled() }}
needs: [merge-playwright-reports]
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Download HTML report artifact
uses: actions/download-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
- name: "Az CLI login"
uses: Azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
- name: Upload Playwright report to blob storage
env:
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
BASE: https://dataexplorerpreview.z5.web.core.windows.net
run: |
az storage blob upload-batch -d '$web' -s playwright-report \
--destination-path "playwright-reports/${KEY}" \
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
--auth-mode login --overwrite true
echo "📊 [Open Playwright report](${BASE}/playwright-reports/${KEY}/index.html)" >> $GITHUB_STEP_SUMMARY
+42 -43
View File
@@ -875,9 +875,10 @@
} }
}, },
"node_modules/@babel/helper-plugin-utils": { "node_modules/@babel/helper-plugin-utils": {
"version": "7.24.8", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
"integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -1752,15 +1753,16 @@
} }
}, },
"node_modules/@babel/plugin-transform-modules-systemjs": { "node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.25.0", "version": "7.29.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
"integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-module-transforms": "^7.25.0", "@babel/helper-module-transforms": "^7.28.6",
"@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-validator-identifier": "^7.24.7", "@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.25.0" "@babel/traverse": "^7.29.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -10991,13 +10993,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.15.0", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.16.0",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"proxy-from-env": "^2.1.0" "proxy-from-env": "^2.1.0"
} }
@@ -15339,9 +15341,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.0", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -15351,7 +15353,8 @@
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/fastify" "url": "https://opencollective.com/fastify"
} }
] ],
"license": "BSD-3-Clause"
}, },
"node_modules/fastest-levenshtein": { "node_modules/fastest-levenshtein": {
"version": "1.0.16", "version": "1.0.16",
@@ -15560,9 +15563,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -15570,6 +15573,7 @@
"url": "https://github.com/sponsors/RubenVerborgh" "url": "https://github.com/sponsors/RubenVerborgh"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
}, },
@@ -22925,7 +22929,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
@@ -25352,25 +25358,16 @@
} }
}, },
"node_modules/recursive-readdir": { "node_modules/recursive-readdir": {
"version": "2.2.2", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
"integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"minimatch": "3.0.4" "minimatch": "^3.0.5"
}, },
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=6.0.0"
}
},
"node_modules/recursive-readdir/node_modules/minimatch": {
"version": "3.0.4",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
} }
}, },
"node_modules/redent": { "node_modules/redent": {
@@ -27498,21 +27495,23 @@
} }
}, },
"node_modules/typedoc/node_modules/brace-expansion": { "node_modules/typedoc/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
}, },
"node_modules/typedoc/node_modules/minimatch": { "node_modules/typedoc/node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
+8 -5
View File
@@ -14,7 +14,7 @@
"http-proxy-middleware": "^3.0.5", "http-proxy-middleware": "^3.0.5",
"node": "^20.19.5", "node": "^20.19.5",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"path-to-regexp": "^0.1.12" "path-to-regexp": "^0.1.13"
} }
}, },
"node_modules/@types/http-proxy": { "node_modules/@types/http-proxy": {
@@ -660,7 +660,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.12", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
@@ -746,9 +748,10 @@
} }
}, },
"node_modules/router/node_modules/path-to-regexp": { "node_modules/router/node_modules/path-to-regexp": {
"version": "8.3.0", "version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
+1 -1
View File
@@ -17,6 +17,6 @@
"http-proxy-middleware": "^3.0.5", "http-proxy-middleware": "^3.0.5",
"node": "^20.19.5", "node": "^20.19.5",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"path-to-regexp": "^0.1.12" "path-to-regexp": "^0.1.13"
} }
} }
+34 -18
View File
@@ -19,22 +19,38 @@ export interface MinimalQueryIterator {
// Pick<QueryIterator<any>, "fetchNext">; // Pick<QueryIterator<any>, "fetchNext">;
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> { export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
TelemetryProcessor.traceStart(Action.ExecuteQuery); const startKey = TelemetryProcessor.traceStart(Action.ExecuteQuery);
return documentsIterator.fetchNext().then((response) => { return documentsIterator
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab }); .fetchNext()
const documents = response.resources; .then((response) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const durationMs = Date.now() - startKey;
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any TelemetryProcessor.traceSuccess(
const itemCount = (documents && documents.length) || 0; Action.ExecuteQuery,
return { { dataExplorerArea: Constants.Areas.Tab, durationMs },
documents, startKey,
hasMoreResults: response.hasMoreResults, );
itemCount, const documents = response.resources;
firstItemIndex: Number(firstItemIndex) + 1, // eslint-disable-next-line @typescript-eslint/no-explicit-any
lastItemIndex: Number(firstItemIndex) + Number(itemCount), const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
headers, const itemCount = (documents && documents.length) || 0;
activityId: response.activityId, return {
requestCharge: response.requestCharge, documents,
}; hasMoreResults: response.hasMoreResults,
}); itemCount,
firstItemIndex: Number(firstItemIndex) + 1,
lastItemIndex: Number(firstItemIndex) + Number(itemCount),
headers,
activityId: response.activityId,
requestCharge: response.requestCharge,
};
})
.catch((error) => {
const durationMs = Date.now() - startKey;
TelemetryProcessor.traceFailure(
Action.ExecuteQuery,
{ dataExplorerArea: Constants.Areas.Tab, durationMs, error: error.message },
startKey,
);
throw error;
});
} }
+54 -5
View File
@@ -1,8 +1,9 @@
import { SeverityLevel } from "@microsoft/applicationinsights-web";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Logger from "../Common/Logger";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { trackTrace } from "../Shared/appInsights";
import { getDataExplorerWindow } from "../Utils/WindowUtils"; import { getDataExplorerWindow } from "../Utils/WindowUtils";
import * as Constants from "./Constants"; import * as Constants from "./Constants";
@@ -97,18 +98,66 @@ const _sendMessage = (message: any): void => {
const portalChildWindow = getDataExplorerWindow(window) || window; const portalChildWindow = getDataExplorerWindow(window) || window;
if (portalChildWindow === window) { if (portalChildWindow === window) {
// Current window is a child of portal, send message to portal window // Current window is a child of portal, send message to portal window
if (portalChildWindow.document.referrer) { const portalTargetOrigin = _getPortalTargetOrigin(portalChildWindow);
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer); if (portalTargetOrigin) {
portalChildWindow.parent.postMessage(message, portalTargetOrigin);
} else { } else {
Logger.logError("Iframe failed to send message to portal", "MessageHandler"); _reportPostMessageFailure("Iframe failed to send message to portal: no target origin");
} }
} else { } else {
// Current window is not a child of portal, send message to the child window instead (which is data explorer) // Current window is not a child of portal, send message to the child window instead (which is data explorer)
if (portalChildWindow.location.origin) { if (portalChildWindow.location.origin) {
portalChildWindow.postMessage(message, portalChildWindow.location.origin); portalChildWindow.postMessage(message, portalChildWindow.location.origin);
} else { } else {
Logger.logError("Iframe failed to send message to data explorer", "MessageHandler"); _reportPostMessageFailure("Iframe failed to send message to data explorer: no origin");
} }
} }
} }
}; };
/**
* Reports a postMessage failure to telemetry without routing through Logger.logError.
*
* IMPORTANT: Logger.logError calls sendMessage, which re-enters _sendMessage. If the
* failure here is itself "no target origin", that would cause infinite recursion
* ("too much recursion"). We call trackTrace directly so telemetry is preserved
* without triggering the recursive postMessage path.
*/
const _reportPostMessageFailure = (errorMessage: string): void => {
try {
trackTrace({ message: errorMessage, severityLevel: SeverityLevel.Error }, { area: "MessageHandler" });
} catch {
// Telemetry should never throw, but guard defensively so we never recurse.
}
};
/**
* Determines the portal's origin to use as postMessage targetOrigin.
*
* Primary source: document.referrer (the origin that loaded this iframe).
*
* Fallback: the `trustedAuthority` query string parameter that the Azure Portal
* embeds in the iframe URL. This is needed because some browser privacy modes
* (notably Firefox 's dynamic state partitioning / ETP Strict) can empty
* document.referrer for cross-origin third-party iframes. Without this fallback,
* postMessage would be called with an empty targetOrigin (which throws) and the
* error logger itself routes through sendMessage, causing infinite recursion.
*
* Returns an empty string when no trustworthy origin can be derived, in which
* case the caller must NOT call postMessage.
*/
const _getPortalTargetOrigin = (childWindow: Window): string => {
const referrer = childWindow.document.referrer;
if (referrer) {
return referrer;
}
try {
const trustedAuthority = new URLSearchParams(childWindow.location.search).get("trustedAuthority");
if (trustedAuthority) {
return new URL(trustedAuthority).origin;
}
} catch {
// Malformed URL — fall through and return empty string.
}
return "";
};
+5 -4
View File
@@ -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,
@@ -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<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",
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<typeof useDatabases.getState>);
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<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",
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" }),
}),
}),
);
});
});
});
+5 -8
View File
@@ -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);
} }
+72 -6
View File
@@ -1,11 +1,15 @@
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { Keys, t } from "Localization";
import { ApiType, userContext } from "UserContext"; import { ApiType, userContext } from "UserContext";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { import {
cancel, cancel,
complete,
create, create,
get, get,
listByDatabaseAccount, listByDatabaseAccount,
pause,
resume,
} from "Utils/arm/generatedClients/dataTransferService/dataTransferJobs"; } from "Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
import { import {
CosmosCassandraDataTransferDataSourceSink, CosmosCassandraDataTransferDataSourceSink,
@@ -31,6 +35,7 @@ export interface DataTransferParams {
sourceCollectionName: string; sourceCollectionName: string;
targetDatabaseName: string; targetDatabaseName: string;
targetCollectionName: string; targetCollectionName: string;
mode?: "Offline" | "Online";
} }
export const getDataTransferJobs = async ( export const getDataTransferJobs = async (
@@ -80,6 +85,7 @@ export const initiateDataTransfer = async (params: DataTransferParams): Promise<
sourceCollectionName, sourceCollectionName,
targetDatabaseName, targetDatabaseName,
targetCollectionName, targetCollectionName,
mode,
} = params; } = params;
const sourcePayload = createPayload(apiType, sourceDatabaseName, sourceCollectionName); const sourcePayload = createPayload(apiType, sourceDatabaseName, sourceCollectionName);
const targetPayload = createPayload(apiType, targetDatabaseName, targetCollectionName); const targetPayload = createPayload(apiType, targetDatabaseName, targetCollectionName);
@@ -87,6 +93,7 @@ export const initiateDataTransfer = async (params: DataTransferParams): Promise<
properties: { properties: {
source: sourcePayload, source: sourcePayload,
destination: targetPayload, destination: targetPayload,
...(mode ? { mode } : {}),
}, },
}; };
return create(subscriptionId, resourceGroupName, accountName, jobName, body); return create(subscriptionId, resourceGroupName, accountName, jobName, body);
@@ -137,30 +144,52 @@ const pollDataTransferJobOperation = async (
if (status === "Cancelled") { if (status === "Cancelled") {
removeFromPolling(jobName); removeFromPolling(jobName);
clearMessage && clearMessage(); clearMessage && clearMessage();
const cancelMessage = `Data transfer job ${jobName} cancelled`; const cancelMessage = t(Keys.containerCopy.dataTransfers.polling.cancelConsoleMessage, { jobName: jobName });
NotificationConsoleUtils.logConsoleError(cancelMessage); NotificationConsoleUtils.logConsoleError(cancelMessage);
throw new AbortError(cancelMessage); throw new AbortError(cancelMessage);
} }
if (status === "Paused") {
removeFromPolling(jobName);
clearMessage && clearMessage();
NotificationConsoleUtils.logConsoleInfo(
t(Keys.containerCopy.dataTransfers.polling.pauseConsoleMessage, { jobName: jobName }),
);
return body;
}
if (status === "Failed" || status === "Faulted") { if (status === "Failed" || status === "Faulted") {
removeFromPolling(jobName); removeFromPolling(jobName);
const errorMessage = body?.properties?.error const errorMessage = body?.properties?.error
? JSON.stringify(body?.properties?.error) ? JSON.stringify(body?.properties?.error)
: "Operation could not be completed"; : t(Keys.containerCopy.dataTransfers.polling.defaultErrorMessage);
const error = new Error(errorMessage); const error = new Error(errorMessage);
clearMessage && clearMessage(); clearMessage && clearMessage();
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} failed: ${errorMessage}`); NotificationConsoleUtils.logConsoleError(
t(Keys.containerCopy.dataTransfers.polling.errorConsoleMessage, {
errorMessage: errorMessage,
jobName: jobName,
}),
);
throw new AbortError(error); throw new AbortError(error);
} }
if (status === "Completed") { if (status === "Completed") {
removeFromPolling(jobName); removeFromPolling(jobName);
clearMessage && clearMessage(); clearMessage && clearMessage();
NotificationConsoleUtils.logConsoleInfo(`Data transfer job ${jobName} completed`); NotificationConsoleUtils.logConsoleInfo(
t(Keys.containerCopy.dataTransfers.polling.completedConsoleMessage, {
jobName: jobName,
}),
);
return body; return body;
} }
const processedCount = body.properties.processedCount; const processedCount = body.properties.processedCount;
const totalCount = body.properties.totalCount; const totalCount = body.properties.totalCount;
const retryMessage = `Data transfer job ${jobName} in progress, total count: ${totalCount}, processed count: ${processedCount}`; throw new Error(
throw new Error(retryMessage); t(Keys.containerCopy.dataTransfers.polling.retryConsoleMessage, {
jobName: jobName,
processedCount: processedCount,
totalCount: totalCount,
}),
);
}; };
export const cancelDataTransferJob = async ( export const cancelDataTransferJob = async (
@@ -174,6 +203,43 @@ export const cancelDataTransferJob = async (
removeFromPolling(cancelResult?.properties?.jobName); removeFromPolling(cancelResult?.properties?.jobName);
}; };
export const pauseDataTransferJob = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
jobName: string,
): Promise<void> => {
const pauseResult: DataTransferJobGetResults = await pause(subscriptionId, resourceGroupName, accountName, jobName);
updateDataTransferJob(pauseResult);
removeFromPolling(pauseResult?.properties?.jobName);
};
export const resumeDataTransferJob = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
jobName: string,
): Promise<void> => {
const resumeResult: DataTransferJobGetResults = await resume(subscriptionId, resourceGroupName, accountName, jobName);
updateDataTransferJob(resumeResult);
};
export const completeDataTransferJob = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
jobName: string,
): Promise<void> => {
const completeResult: DataTransferJobGetResults = await complete(
subscriptionId,
resourceGroupName,
accountName,
jobName,
);
updateDataTransferJob(completeResult);
removeFromPolling(completeResult?.properties?.jobName);
};
const createPayload = ( const createPayload = (
apiType: ApiType, apiType: ApiType,
databaseName: string, databaseName: string,
+148 -1
View File
@@ -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, readDatabasesWithARM } from "./readDatabases";
describe("readDatabases", () => { describe("readDatabases", () => {
beforeAll(() => { beforeAll(() => {
@@ -42,3 +43,149 @@ describe("readDatabases", () => {
expect(client).toHaveBeenCalled(); 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");
});
});
+12 -5
View File
@@ -4,7 +4,7 @@ import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; 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 { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@@ -96,10 +96,17 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
return databases; return databases;
} }
async function readDatabasesWithARM(): Promise<DataModels.Database[]> { export async function readDatabasesWithARM(accountOverride?: {
subscriptionId: string;
resourceGroup: string;
accountName: string;
apiType?: ApiType;
}): Promise<DataModels.Database[]> {
let rpResponse; let rpResponse;
const { subscriptionId, resourceGroup, apiType, databaseAccount } = userContext; const subscriptionId = accountOverride?.subscriptionId ?? userContext.subscriptionId ?? "";
const accountName = databaseAccount.name; const resourceGroup = accountOverride?.resourceGroup ?? userContext.resourceGroup ?? "";
const accountName = accountOverride?.accountName ?? userContext?.databaseAccount?.name ?? "";
const apiType = accountOverride?.apiType ?? userContext.apiType;
switch (apiType) { switch (apiType) {
case "SQL": case "SQL":
@@ -118,5 +125,5 @@ async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
throw new Error(`Unsupported default experience type: ${apiType}`); 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) ?? [];
} }
+12
View File
@@ -406,11 +406,22 @@ export interface AutoPilotOfferSettings {
targetMaxThroughput?: number; targetMaxThroughput?: number;
} }
export interface AccountOverride {
subscriptionId: string;
resourceGroup: string;
accountName: string;
capabilities: Capability[];
capacityMode?: CapacityMode;
enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean;
}
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 {
@@ -430,6 +441,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",
@@ -1,4 +1,5 @@
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers"; import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
@@ -15,7 +16,6 @@ import {
CreateJobRequest, CreateJobRequest,
DataTransferJobGetResults, DataTransferJobGetResults,
} from "../../../Utils/arm/generatedClients/dataTransferService/types"; } from "../../../Utils/arm/generatedClients/dataTransferService/types";
import ContainerCopyMessages from "../ContainerCopyMessages";
import { import {
convertTime, convertTime,
convertToCamelCase, convertToCamelCase,
@@ -35,7 +35,7 @@ export const openCreateCopyJobPanel = (explorer: Explorer) => {
const sidePanelState = useSidePanel.getState(); const sidePanelState = useSidePanel.getState();
sidePanelState.setPanelHasConsole(false); sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel( sidePanelState.openSidePanel(
ContainerCopyMessages.createCopyJobPanelTitle, t(Keys.containerCopy.createCopyJob.panelTitle),
<CreateCopyJobScreensProvider explorer={explorer} />, <CreateCopyJobScreensProvider explorer={explorer} />,
"650px", "650px",
); );
@@ -45,7 +45,7 @@ export const openCopyJobDetailsPanel = (job: CopyJobType) => {
const sidePanelState = useSidePanel.getState(); const sidePanelState = useSidePanel.getState();
sidePanelState.setPanelHasConsole(false); sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel( sidePanelState.openSidePanel(
ContainerCopyMessages.copyJobDetailsPanelTitle(job.Name), job.Name || t(Keys.containerCopy.jobDetails.panelTitleDefault),
<CopyJobDetails job={job} />, <CopyJobDetails job={job} />,
"650px", "650px",
); );
@@ -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,
}, },
@@ -193,7 +193,7 @@ export const updateCopyJobStatus = async (job: CopyJobType, action: string): Pro
const pattern = new RegExp(`'(${statusList.join("|")})'`, "g"); const pattern = new RegExp(`'(${statusList.join("|")})'`, "g");
const normalizedErrorMessage = errorMessage.replace( const normalizedErrorMessage = errorMessage.replace(
pattern, pattern,
`'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`, `'${t(Keys.containerCopy.monitorJobs.status.inProgress)}'`,
); );
logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus"); logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus");
throw error; throw error;
@@ -5,10 +5,10 @@ import RefreshIcon from "../../../../images/refresh-cosmos.svg";
import SunIcon from "../../../../images/SunIcon.svg"; import SunIcon from "../../../../images/SunIcon.svg";
import { configContext, Platform } from "../../../ConfigContext"; import { configContext, Platform } from "../../../ConfigContext";
import { useThemeStore } from "../../../hooks/useTheme"; import { useThemeStore } from "../../../hooks/useTheme";
import { Keys, t } from "Localization";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as Actions from "../Actions/CopyJobActions"; import * as Actions from "../Actions/CopyJobActions";
import ContainerCopyMessages from "../ContainerCopyMessages";
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState"; import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes"; import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
@@ -19,15 +19,15 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
{ {
key: "createCopyJob", key: "createCopyJob",
iconSrc: AddIcon, iconSrc: AddIcon,
label: ContainerCopyMessages.createCopyJobButtonLabel, label: t(Keys.containerCopy.commandBar.createCopyJobButtonLabel),
ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel, ariaLabel: t(Keys.containerCopy.commandBar.createCopyJobButtonAriaLabel),
onClick: () => Actions.openCreateCopyJobPanel(explorer), onClick: () => Actions.openCreateCopyJobPanel(explorer),
}, },
{ {
key: "refresh", key: "refresh",
iconSrc: RefreshIcon, iconSrc: RefreshIcon,
label: ContainerCopyMessages.refreshButtonLabel, label: t(Keys.common.refresh),
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel, ariaLabel: t(Keys.containerCopy.commandBar.refreshButtonAriaLabel),
onClick: () => monitorCopyJobsRef?.refreshJobList(), onClick: () => monitorCopyJobsRef?.refreshJobList(),
}, },
{ {
@@ -48,8 +48,8 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
buttons.push({ buttons.push({
key: "feedback", key: "feedback",
iconSrc: FeedbackIcon, iconSrc: FeedbackIcon,
label: ContainerCopyMessages.feedbackButtonLabel, label: t(Keys.containerCopy.commandBar.feedbackButtonLabel),
ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel, ariaLabel: t(Keys.containerCopy.commandBar.feedbackButtonAriaLabel),
onClick: () => { onClick: () => {
explorer.openContainerCopyFeedbackBlade(); explorer.openContainerCopyFeedbackBlade();
}, },
@@ -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 dont 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",
},
},
};
@@ -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,7 +69,13 @@ describe("CopyJobContext", () => {
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, target: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
}); });
expect(contextValue.flow).toBeNull(); expect(contextValue.flow).toBeNull();
expect(contextValue.contextError).toBeNull(); expect(contextValue.contextError).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,11 +616,11 @@ 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 sourceReadWriteAccessFromTarget as false", () => {
let contextValue: any; let contextValue: any;
render( render(
@@ -634,7 +634,7 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>, </CopyJobContextProvider>,
); );
expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false); expect(contextValue.copyJobState.sourceReadWriteAccessFromTarget).toBe(false);
}); });
it("should initialize with empty database and container ids", () => { it("should initialize with empty database and container ids", () => {
@@ -23,18 +23,18 @@ const getInitialCopyJobState = (): CopyJobContextState => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: userContext.subscriptionId || "", subscriptionId: userContext.subscriptionId || "",
account: userContext.databaseAccount || null, account: userContext.databaseAccount || null,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, target: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
}; };
}; };
@@ -2,9 +2,9 @@ import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes"; import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils"; import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import AddManagedIdentity from "./AddManagedIdentity"; import AddManagedIdentity from "./AddManagedIdentity";
@@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => {
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}; };
const mockContextValue = { const mockContextValue = {
@@ -133,16 +133,16 @@ describe("AddManagedIdentity", () => {
it("renders all required elements", () => { it("renders all required elements", () => {
renderWithContext(); renderWithContext();
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.description)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.description))).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.addManagedIdentity.descriptionHrefText))).toBeInTheDocument();
expect(screen.getByRole("switch")).toBeInTheDocument(); expect(screen.getByRole("switch")).toBeInTheDocument();
}); });
it("renders description link with correct href", () => { it("renders description link with correct href", () => {
renderWithContext(); renderWithContext();
const link = screen.getByText(ContainerCopyMessages.addManagedIdentity.descriptionHrefText); const link = screen.getByText(t(Keys.containerCopy.addManagedIdentity.descriptionHrefText));
expect(link.closest("a")).toHaveAttribute("href", ContainerCopyMessages.addManagedIdentity.descriptionHref); expect(link.closest("a")).toHaveAttribute("href", t(Keys.containerCopy.addManagedIdentity.descriptionHref));
expect(link.closest("a")).toHaveAttribute("target", "_blank"); expect(link.closest("a")).toHaveAttribute("target", "_blank");
expect(link.closest("a")).toHaveAttribute("rel", "noopener noreferrer"); expect(link.closest("a")).toHaveAttribute("rel", "noopener noreferrer");
}); });
@@ -175,7 +175,7 @@ describe("AddManagedIdentity", () => {
const toggle = screen.getByRole("switch"); const toggle = screen.getByRole("switch");
fireEvent.click(toggle); 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", () => { it("hides popover when toggle is off", () => {
@@ -185,7 +185,7 @@ describe("AddManagedIdentity", () => {
fireEvent.click(toggle); fireEvent.click(toggle);
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", () => { it("displays correct enablement description with account name", () => {
const expectedDescription = ContainerCopyMessages.addManagedIdentity.enablementDescription( const expectedDescription = t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
mockCopyJobState.target.account.name, accountName: mockCopyJobState.source.account.name,
); });
expect(screen.getByText(expectedDescription)).toBeInTheDocument(); expect(screen.getByText(expectedDescription)).toBeInTheDocument();
}); });
@@ -220,7 +220,7 @@ describe("AddManagedIdentity", () => {
const cancelButton = screen.getByText("Cancel"); const cancelButton = screen.getByText("Cancel");
fireEvent.click(cancelButton); 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"); const toggle = screen.getByRole("switch");
expect(toggle).not.toBeChecked(); expect(toggle).not.toBeChecked();
@@ -1,7 +1,7 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react"; import { Link, Stack, Text, Toggle } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils"; import { updateSystemIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import InfoTooltip from "../Components/InfoTooltip"; import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer"; import PopoverMessage from "../Components/PopoverContainer";
@@ -11,14 +11,14 @@ import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ( const managedIdentityTooltip = (
<Text> <Text>
{ContainerCopyMessages.addManagedIdentity.tooltip.content} &nbsp; {t(Keys.containerCopy.addManagedIdentity.tooltipContent)} &nbsp;
<Link <Link
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.addManagedIdentity.tooltip.href} href={t(Keys.containerCopy.addManagedIdentity.tooltipHref)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText} {t(Keys.containerCopy.addManagedIdentity.tooltipHrefText)}
</Link> </Link>
</Text> </Text>
); );
@@ -32,9 +32,9 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
return ( return (
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Text className="themeText"> <Text className="themeText">
{ContainerCopyMessages.addManagedIdentity.description}&ensp; {t(Keys.containerCopy.addManagedIdentity.description)}&ensp;
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer"> <Link href={t(Keys.containerCopy.addManagedIdentity.descriptionHref)} target="_blank" rel="noopener noreferrer">
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText} {t(Keys.containerCopy.addManagedIdentity.descriptionHrefText)}
</Link>{" "} </Link>{" "}
&nbsp; &nbsp;
<InfoTooltip content={managedIdentityTooltip} /> <InfoTooltip content={managedIdentityTooltip} />
@@ -42,18 +42,20 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<Toggle <Toggle
data-test="btn-toggle" data-test="btn-toggle"
checked={systemAssigned} checked={systemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={t(Keys.common.on)}
offText={ContainerCopyMessages.toggleBtn.offText} offText={t(Keys.common.off)}
onChange={onToggle} onChange={onToggle}
/> />
<PopoverMessage <PopoverMessage
isLoading={loading} isLoading={loading}
visible={systemAssigned} visible={systemAssigned}
title={ContainerCopyMessages.addManagedIdentity.enablementTitle} title={t(Keys.containerCopy.addManagedIdentity.enablementTitle)}
onCancel={() => onToggle(null, false)} onCancel={() => onToggle(null, false)}
onPrimary={handleAddSystemIdentity} onPrimary={handleAddSystemIdentity}
> >
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)} {t(Keys.containerCopy.addManagedIdentity.enablementDescription, {
accountName: copyJobState.source?.account?.name,
})}
</PopoverMessage> </PopoverMessage>
</Stack> </Stack>
); );
@@ -1,10 +1,10 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes"; import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes";
import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity"; import AddReadWritePermissionToDefaultIdentity from "./AddReadWritePermissionToDefaultIdentity";
jest.mock("../../../../../Common/Logger", () => ({ jest.mock("../../../../../Common/Logger", () => ({
logError: jest.fn(), logError: jest.fn(),
@@ -73,7 +73,7 @@ import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUti
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import useToggle from "./hooks/useToggle"; import useToggle from "./hooks/useToggle";
describe("AddReadPermissionToDefaultIdentity Component", () => { describe("AddReadWritePermissionToDefaultIdentity Component", () => {
const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>; const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>; const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>;
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction< const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
@@ -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",
@@ -96,12 +96,16 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
properties: { properties: {
documentEndpoint: "https://source-account.documents.azure.com:443/", documentEndpoint: "https://source-account.documents.azure.com:443/",
}, },
identity: {
principalId: "source-principal-id",
type: "SystemAssigned",
},
}, },
databaseId: "source-db", databaseId: "source-db",
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",
@@ -119,7 +123,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
databaseId: "target-db", databaseId: "target-db",
containerId: "target-container", containerId: "target-container",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}, },
setCopyJobState: jest.fn(), setCopyJobState: jest.fn(),
setContextError: jest.fn(), setContextError: jest.fn(),
@@ -133,7 +137,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
const renderComponent = (contextValue = mockContextValue) => { const renderComponent = (contextValue = mockContextValue) => {
return render( return render(
<CopyJobContext.Provider value={contextValue}> <CopyJobContext.Provider value={contextValue}>
<AddReadPermissionToDefaultIdentity /> <AddReadWritePermissionToDefaultIdentity />
</CopyJobContext.Provider>, </CopyJobContext.Provider>,
); );
}; };
@@ -164,12 +168,12 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it("should render correctly when sourceReadAccessFromTarget is true", () => { it("should render correctly when sourceReadWriteAccessFromTarget is true", () => {
const contextWithAccess = { const contextWithAccess = {
...mockContextValue, ...mockContextValue,
copyJobState: { copyJobState: {
...mockContextValue.copyJobState, ...mockContextValue.copyJobState,
sourceReadAccessFromTarget: true, sourceReadWriteAccessFromTarget: true,
}, },
}; };
const { container } = renderComponent(contextWithAccess); const { container } = renderComponent(contextWithAccess);
@@ -180,7 +184,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
describe("Component Structure", () => { describe("Component Structure", () => {
it("should display the description text", () => { it("should display the description text", () => {
renderComponent(); renderComponent();
expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.readWritePermissionAssigned.description))).toBeInTheDocument();
}); });
it("should display the info tooltip", () => { it("should display the info tooltip", () => {
@@ -212,10 +216,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(screen.getByTestId("popover-message")).toBeInTheDocument(); expect(screen.getByTestId("popover-message")).toBeInTheDocument();
expect(screen.getByTestId("popover-title")).toHaveTextContent( expect(screen.getByTestId("popover-title")).toHaveTextContent(
ContainerCopyMessages.readPermissionAssigned.popoverTitle, t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle),
); );
expect(screen.getByTestId("popover-content")).toHaveTextContent( 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); 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({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
@@ -258,22 +262,22 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => { await waitFor(() => {
expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith( 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(() => { beforeEach(() => {
mockUseToggle.mockReturnValue([true, jest.fn()]); mockUseToggle.mockReturnValue([true, jest.fn()]);
}); });
it("should successfully assign role and update context", async () => { it("should successfully assign role and update context", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
@@ -284,10 +288,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => { await waitFor(() => {
expect(mockAssignRole).toHaveBeenCalledWith( expect(mockAssignRole).toHaveBeenCalledWith(
"source-sub-id", "target-sub-id",
"source-rg", "target-rg",
"source-account", "target-account",
"target-principal-id", "source-principal-id",
); );
}); });
@@ -298,9 +302,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
it("should handle error when assignRole fails", async () => { it("should handle error when assignRole fails", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockRejectedValue(new Error("Permission denied")); mockAssignRole.mockRejectedValue(new Error("Permission denied"));
@@ -312,7 +316,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => { await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith( expect(mockLogError).toHaveBeenCalledWith(
"Permission denied", "Permission denied",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission", "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
); );
}); });
@@ -323,9 +327,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
it("should handle error without message", async () => { it("should handle error without message", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockRejectedValue({}); mockAssignRole.mockRejectedValue({});
@@ -336,23 +340,23 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => { await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith( expect(mockLogError).toHaveBeenCalledWith(
"Error assigning read permission to default identity. Please try again later.", "Error assigning read-write permission to default identity. Please try again later.",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission", "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
); );
}); });
await waitFor(() => { await waitFor(() => {
expect(mockContextValue.setContextError).toHaveBeenCalledWith( 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 () => { it("should show loading state during role assignment", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockImplementation( mockAssignRole.mockImplementation(
@@ -371,9 +375,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
it.skip("should not assign role when assignRole returns falsy", async () => { it.skip("should not assign role when assignRole returns falsy", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue(null); mockAssignRole.mockResolvedValue(null);
@@ -431,10 +435,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
...mockContextValue, ...mockContextValue,
copyJobState: { copyJobState: {
...mockContextValue.copyJobState, ...mockContextValue.copyJobState,
target: { source: {
...mockContextValue.copyJobState.target, ...mockContextValue.copyJobState.source,
account: { account: {
...mockContextValue.copyJobState.target.account!, ...mockContextValue.copyJobState.source.account!,
identity: { identity: {
principalId: "", principalId: "",
type: "SystemAssigned", type: "SystemAssigned",
@@ -446,9 +450,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
mockUseToggle.mockReturnValue([true, jest.fn()]); mockUseToggle.mockReturnValue([true, jest.fn()]);
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
@@ -458,7 +462,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
fireEvent.click(primaryButton); fireEvent.click(primaryButton);
await waitFor(() => { 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()]); mockUseToggle.mockReturnValue([true, jest.fn()]);
mockGetAccountDetailsFromResourceId.mockReturnValue({ mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id", subscriptionId: "target-sub-id",
resourceGroup: "source-rg", resourceGroup: "target-rg",
accountName: "source-account", accountName: "target-account",
}); });
mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType);
@@ -496,7 +500,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(updatedState).toEqual({ expect(updatedState).toEqual({
...mockContextValue.copyJobState, ...mockContextValue.copyJobState,
sourceReadAccessFromTarget: true, sourceReadWriteAccessFromTarget: true,
}); });
}); });
}); });
@@ -1,8 +1,8 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react"; import { Link, Stack, Text, Toggle } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import { assignRole } from "../../../../../Utils/arm/RbacUtils"; import { assignRole } from "../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import InfoTooltip from "../Components/InfoTooltip"; import InfoTooltip from "../Components/InfoTooltip";
@@ -12,51 +12,54 @@ import useToggle from "./hooks/useToggle";
const TooltipContent = ( const TooltipContent = (
<Text> <Text>
{ContainerCopyMessages.readPermissionAssigned.tooltip.content} &nbsp; {t(Keys.containerCopy.readWritePermissionAssigned.tooltipContent)} &nbsp;
<Link <Link
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href} href={t(Keys.containerCopy.readWritePermissionAssigned.tooltipHref)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText} {t(Keys.containerCopy.readWritePermissionAssigned.tooltipHrefText)}
</Link> </Link>
</Text> </Text>
); );
type AddReadPermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIdentityProps> = () => { type AddReadWritePermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionToDefaultIdentityProps> = () => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext(); 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 { source, target } = copyJobState;
const selectedSourceAccount = source?.account; const selectedTargetAccount = target?.account;
try { try {
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: targetSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: targetResourceGroup,
accountName: sourceAccountName, accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
setLoading(true); setLoading(true);
const assignedRole = await assignRole( const assignedRole = await assignRole(
sourceSubscriptionId, targetSubscriptionId,
sourceResourceGroup, targetResourceGroup,
sourceAccountName, targetAccountName,
target?.account?.identity?.principalId ?? "", source?.account?.identity?.principalId ?? "",
); );
if (assignedRole) { if (assignedRole) {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
...prevState, ...prevState,
sourceReadAccessFromTarget: true, sourceReadWriteAccessFromTarget: true,
})); }));
} }
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error.message || "Error assigning read permission to default identity. Please try again later."; error.message || "Error assigning read-write permission to default identity. Please try again later.";
logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission"); logError(errorMessage, "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission");
setContextError(errorMessage); setContextError(errorMessage);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -66,14 +69,14 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
return ( return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Text className="toggle-label"> <Text className="toggle-label">
{ContainerCopyMessages.readPermissionAssigned.description}&ensp; {t(Keys.containerCopy.readWritePermissionAssigned.description)}&ensp;
<InfoTooltip content={TooltipContent} /> <InfoTooltip content={TooltipContent} />
</Text> </Text>
<Toggle <Toggle
data-test="btn-toggle" data-test="btn-toggle"
checked={readPermissionAssigned} checked={readWritePermissionAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={t(Keys.common.on)}
offText={ContainerCopyMessages.toggleBtn.offText} offText={t(Keys.common.off)}
onChange={onToggle} onChange={onToggle}
inlineLabel inlineLabel
styles={{ styles={{
@@ -83,15 +86,15 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
/> />
<PopoverMessage <PopoverMessage
isLoading={loading} isLoading={loading}
visible={readPermissionAssigned} visible={readWritePermissionAssigned}
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle} title={t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle)}
onCancel={() => onToggle(null, false)} onCancel={() => onToggle(null, false)}
onPrimary={handleAddReadPermission} onPrimary={handleAddReadWritePermission}
> >
{ContainerCopyMessages.readPermissionAssigned.popoverDescription} {t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription)}
</PopoverMessage> </PopoverMessage>
</Stack> </Stack>
); );
}; };
export default AddReadPermissionToDefaultIdentity; export default AddReadWritePermissionToDefaultIdentity;
@@ -1,7 +1,7 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { render, RenderResult } from "@testing-library/react"; import { render, RenderResult } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes"; import { CopyJobContextProviderType, CopyJobContextState } from "../../../Types/CopyJobTypes";
@@ -43,12 +43,12 @@ jest.mock("./AddManagedIdentity", () => {
return MockAddManagedIdentity; return MockAddManagedIdentity;
}); });
jest.mock("./AddReadPermissionToDefaultIdentity", () => { jest.mock("./AddReadWritePermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => { const MockAddReadWritePermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">Add Read Permission Component</div>; return <div data-testid="add-read-write-permission">Add Read-Write Permission Component</div>;
}; };
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity"; MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity; return MockAddReadWritePermissionToDefaultIdentity;
}); });
jest.mock("./DefaultManagedIdentity", () => { jest.mock("./DefaultManagedIdentity", () => {
@@ -85,18 +85,18 @@ 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",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
...overrides, ...overrides,
}); });
@@ -154,7 +154,7 @@ describe("AssignPermissions Component", () => {
const copyJobState = createMockCopyJobState(); const copyJobState = createMockCopyJobState();
const { getByText } = renderWithContext(copyJobState); 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 () => { it("should display intra account description for same accounts with online migration", async () => {
@@ -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",
@@ -179,7 +179,9 @@ describe("AssignPermissions Component", () => {
const { getByText } = renderWithContext(copyJobState); const { getByText } = renderWithContext(copyJobState);
expect( expect(
getByText(ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription("Same Account")), getByText(
t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, { accountName: "Same Account" }),
),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });
@@ -201,7 +203,7 @@ describe("AssignPermissions Component", () => {
completed: true, completed: true,
}, },
{ {
id: "readPermissionAssigned", id: "readWritePermissionAssigned",
title: "Read Permission Assigned", title: "Read Permission Assigned",
Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>, Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>,
disabled: false, disabled: false,
@@ -347,7 +349,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",
@@ -1,10 +1,10 @@
import { Image, Stack, Text } from "@fluentui/react"; import { Image, Stack, Text } from "@fluentui/react";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components"; import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
import { Keys, t } from "Localization";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg"; import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
import WarningIcon from "../../../../../../images/warning.svg"; import WarningIcon from "../../../../../../images/warning.svg";
import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree"; import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { isIntraAccountCopy } from "../../../CopyJobUtils"; import { isIntraAccountCopy } from "../../../CopyJobUtils";
import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums";
@@ -106,11 +106,11 @@ const AssignPermissions = () => {
tokens={{ childrenGap: 20 }} tokens={{ childrenGap: 20 }}
> >
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online {isSameAccount && copyJobState?.migrationType === CopyJobMigrationType.Online
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription( ? t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, {
copyJobState?.source?.account?.name || "", accountName: copyJobState?.source?.account?.name || "",
) })
: ContainerCopyMessages.assignPermissions.crossAccountDescription} : t(Keys.containerCopy.assignPermissions.crossAccountDescription)}
</Text> </Text>
{totalSectionsCount === 0 ? ( {totalSectionsCount === 0 ? (
@@ -1,8 +1,8 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils"; import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import DefaultManagedIdentity from "./DefaultManagedIdentity"; import DefaultManagedIdentity from "./DefaultManagedIdentity";
@@ -69,6 +69,12 @@ const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
describe("DefaultManagedIdentity", () => { describe("DefaultManagedIdentity", () => {
const mockCopyJobContextValue = { const mockCopyJobContextValue = {
copyJobState: { copyJobState: {
source: {
account: {
name: "test-cosmos-account",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos-account",
},
},
target: { target: {
account: { account: {
name: "test-cosmos-account", name: "test-cosmos-account",
@@ -166,7 +172,7 @@ describe("DefaultManagedIdentity", () => {
expect(popover).toBeInTheDocument(); expect(popover).toBeInTheDocument();
const title = screen.getByTestId("popover-title"); 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"); const content = screen.getByTestId("popover-content");
expect(content).toHaveTextContent( expect(content).toHaveTextContent(
@@ -260,6 +266,12 @@ describe("DefaultManagedIdentity", () => {
const contextValueWithoutAccount = { const contextValueWithoutAccount = {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
source: {
account: {
name: "",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/",
},
},
target: { target: {
account: { account: {
name: "", name: "",
@@ -277,6 +289,9 @@ describe("DefaultManagedIdentity", () => {
const contextValueWithNullAccount = { const contextValueWithNullAccount = {
...mockCopyJobContextValue, ...mockCopyJobContextValue,
copyJobState: { copyJobState: {
source: {
account: null as DatabaseAccount | null,
},
target: { target: {
account: null as DatabaseAccount | null, account: null as DatabaseAccount | null,
}, },
@@ -339,8 +354,8 @@ describe("DefaultManagedIdentity", () => {
it("should display correct toggle button text", () => { it("should display correct toggle button text", () => {
renderComponent(); renderComponent();
const onText = screen.queryByText(ContainerCopyMessages.toggleBtn.onText); const onText = screen.queryByText(t(Keys.common.on));
const offText = screen.queryByText(ContainerCopyMessages.toggleBtn.offText); const offText = screen.queryByText(t(Keys.common.off));
expect(onText || offText).toBeTruthy(); expect(onText || offText).toBeTruthy();
}); });
@@ -348,7 +363,7 @@ describe("DefaultManagedIdentity", () => {
it("should display correct link text in tooltip", () => { it("should display correct link text in tooltip", () => {
renderComponent(); renderComponent();
const linkText = screen.getByText(ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText); const linkText = screen.getByText(t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText));
expect(linkText).toBeInTheDocument(); expect(linkText).toBeInTheDocument();
}); });
}); });
@@ -1,7 +1,7 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react"; import { Link, Stack, Text, Toggle } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils"; import { updateDefaultIdentity } from "../../../../../Utils/arm/identityUtils";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import InfoTooltip from "../Components/InfoTooltip"; import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer"; import PopoverMessage from "../Components/PopoverContainer";
@@ -11,14 +11,14 @@ import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ( const managedIdentityTooltip = (
<Text> <Text>
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content} &nbsp; {t(Keys.containerCopy.defaultManagedIdentity.tooltipContent)} &nbsp;
<Link <Link
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href} href={t(Keys.containerCopy.defaultManagedIdentity.tooltipHref)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText} {t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText)}
</Link> </Link>
</Text> </Text>
); );
@@ -32,14 +32,17 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
return ( return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label"> <div className="toggle-label">
{ContainerCopyMessages.defaultManagedIdentity.description(copyJobState?.target?.account?.name)} &nbsp; {t(Keys.containerCopy.defaultManagedIdentity.description, {
accountName: copyJobState?.source?.account?.name,
})}{" "}
&nbsp;
<InfoTooltip content={managedIdentityTooltip} /> <InfoTooltip content={managedIdentityTooltip} />
</div> </div>
<Toggle <Toggle
data-test="btn-toggle" data-test="btn-toggle"
checked={defaultSystemAssigned} checked={defaultSystemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText} onText={t(Keys.common.on)}
offText={ContainerCopyMessages.toggleBtn.offText} offText={t(Keys.common.off)}
onChange={onToggle} onChange={onToggle}
inlineLabel inlineLabel
styles={{ styles={{
@@ -50,11 +53,13 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<PopoverMessage <PopoverMessage
isLoading={loading} isLoading={loading}
visible={defaultSystemAssigned} visible={defaultSystemAssigned}
title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle} title={t(Keys.containerCopy.defaultManagedIdentity.popoverTitle)}
onCancel={() => onToggle(null, false)} onCancel={() => onToggle(null, false)}
onPrimary={handleAddSystemIdentity} onPrimary={handleAddSystemIdentity}
> >
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription(copyJobState?.target?.account?.name)} {t(Keys.containerCopy.defaultManagedIdentity.popoverDescription, {
accountName: copyJobState?.source?.account?.name,
})}
</PopoverMessage> </PopoverMessage>
</Stack> </Stack>
); );
@@ -2,12 +2,12 @@ import "@testing-library/jest-dom";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes"; import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { CapabilityNames } from "../../../../../Common/Constants"; import { CapabilityNames } from "../../../../../Common/Constants";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../Context/CopyJobContext";
import OnlineCopyEnabled from "./OnlineCopyEnabled"; import OnlineCopyEnabled from "./OnlineCopyEnabled";
@@ -97,7 +97,9 @@ describe("OnlineCopyEnabled", () => {
it("should render the description with account name", () => { it("should render the description with account name", () => {
renderComponent(); 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(); expect(description).toBeInTheDocument();
}); });
@@ -105,10 +107,10 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const link = screen.getByRole("link", { const link = screen.getByRole("link", {
name: ContainerCopyMessages.onlineCopyEnabled.hrefText, name: t(Keys.containerCopy.onlineCopyEnabled.hrefText),
}); });
expect(link).toBeInTheDocument(); 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("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer"); expect(link).toHaveAttribute("rel", "noopener noreferrer");
}); });
@@ -117,7 +119,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
expect(button).not.toBeDisabled(); expect(button).not.toBeDisabled();
@@ -134,7 +136,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const refreshButton = screen.queryByRole("button", { const refreshButton = screen.queryByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel, name: t(Keys.common.refresh),
}); });
expect(refreshButton).not.toBeInTheDocument(); expect(refreshButton).not.toBeInTheDocument();
}); });
@@ -167,7 +169,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -222,7 +224,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -246,7 +248,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -259,7 +261,9 @@ describe("OnlineCopyEnabled", () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel("test-account")), screen.getByText(
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { accountName: "test-account" }),
),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });
@@ -272,7 +276,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -306,7 +310,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -318,7 +322,7 @@ describe("OnlineCopyEnabled", () => {
}); });
const refreshButton = screen.getByRole("button", { const refreshButton = screen.getByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel, name: t(Keys.common.refresh),
}); });
await act(async () => { await act(async () => {
@@ -349,7 +353,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -379,7 +383,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -401,7 +405,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -418,7 +422,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -436,7 +440,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
await act(async () => { await act(async () => {
@@ -450,7 +454,7 @@ describe("OnlineCopyEnabled", () => {
mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {})); mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {}));
const refreshButton = screen.getByRole("button", { const refreshButton = screen.getByRole("button", {
name: ContainerCopyMessages.refreshButtonLabel, name: t(Keys.common.refresh),
}); });
await act(async () => { await act(async () => {
@@ -536,7 +540,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(contextWithNoCapabilities); renderComponent(contextWithNoCapabilities);
const enableButton = screen.getByRole("button", { const enableButton = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
expect(enableButton).toBeInTheDocument(); expect(enableButton).toBeInTheDocument();
}); });
@@ -547,7 +551,7 @@ describe("OnlineCopyEnabled", () => {
renderComponent(); renderComponent();
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.onlineCopyEnabled.buttonText, name: t(Keys.containerCopy.onlineCopyEnabled.buttonText),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
}); });
@@ -1,12 +1,12 @@
import { Link, PrimaryButton, Stack } from "@fluentui/react"; import { Link, PrimaryButton, Stack } from "@fluentui/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { CapabilityNames } from "../../../../../Common/Constants"; import { CapabilityNames } from "../../../../../Common/Constants";
import LoadingOverlay from "../../../../../Common/LoadingOverlay"; import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update as updateDatabaseAccount } from "../../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import { AccountValidatorFn } from "../../../Types/CopyJobTypes"; import { AccountValidatorFn } from "../../../Types/CopyJobTypes";
@@ -76,21 +76,25 @@ const OnlineCopyEnabled: React.FC = () => {
setShowRefreshButton(false); setShowRefreshButton(false);
try { try {
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel); setLoaderMessage(t(Keys.containerCopy.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel));
const sourAccountBeforeUpdate = await fetchDatabaseAccount( const sourAccountBeforeUpdate = await fetchDatabaseAccount(
sourceSubscriptionId, sourceSubscriptionId,
sourceResourceGroup, sourceResourceGroup,
sourceAccountName, sourceAccountName,
); );
if (!sourAccountBeforeUpdate?.properties.enableAllVersionsAndDeletesChangeFeed) { if (!sourAccountBeforeUpdate?.properties.enableAllVersionsAndDeletesChangeFeed) {
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel); setLoaderMessage(t(Keys.containerCopy.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel));
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
properties: { properties: {
enableAllVersionsAndDeletesChangeFeed: true, enableAllVersionsAndDeletesChangeFeed: true,
}, },
}); });
} }
setLoaderMessage(ContainerCopyMessages.onlineCopyEnabled.enablingOnlineCopySpinnerLabel(sourceAccountName)); setLoaderMessage(
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, {
accountName: sourceAccountName,
}),
);
await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, {
properties: { properties: {
capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }], capabilities: [...sourceAccountCapabilities, { name: CapabilityNames.EnableOnlineCopyFeature }],
@@ -132,16 +136,16 @@ const OnlineCopyEnabled: React.FC = () => {
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<LoadingOverlay isLoading={loading} label={loaderMessage} /> <LoadingOverlay isLoading={loading} label={loaderMessage} />
<Stack.Item className="info-message"> <Stack.Item className="info-message">
{ContainerCopyMessages.onlineCopyEnabled.description(source?.account?.name || "")}&ensp; {t(Keys.containerCopy.onlineCopyEnabled.description, { accountName: source?.account?.name || "" })}&ensp;
<Link href={ContainerCopyMessages.onlineCopyEnabled.href} target="_blank" rel="noopener noreferrer"> <Link href={t(Keys.containerCopy.onlineCopyEnabled.href)} target="_blank" rel="noopener noreferrer">
{ContainerCopyMessages.onlineCopyEnabled.hrefText} {t(Keys.containerCopy.onlineCopyEnabled.hrefText)}
</Link> </Link>
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>
{showRefreshButton ? ( {showRefreshButton ? (
<PrimaryButton <PrimaryButton
className="fullWidth" className="fullWidth"
text={ContainerCopyMessages.refreshButtonLabel} text={t(Keys.common.refresh)}
iconProps={{ iconName: "Refresh" }} iconProps={{ iconName: "Refresh" }}
onClick={handleRefresh} onClick={handleRefresh}
disabled={loading} disabled={loading}
@@ -149,7 +153,7 @@ const OnlineCopyEnabled: React.FC = () => {
) : ( ) : (
<PrimaryButton <PrimaryButton
className="fullWidth" className="fullWidth"
text={loading ? "" : ContainerCopyMessages.onlineCopyEnabled.buttonText} text={loading ? "" : t(Keys.containerCopy.onlineCopyEnabled.buttonText)}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})} {...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
disabled={loading} disabled={loading}
onClick={handleOnlineCopyEnable} onClick={handleOnlineCopyEnable}
@@ -50,18 +50,18 @@ 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",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
} as CopyJobContextState; } as CopyJobContextState;
const mockSetCopyJobState = jest.fn(); const mockSetCopyJobState = jest.fn();
@@ -1,10 +1,10 @@
import { Link, PrimaryButton, Stack, Text } from "@fluentui/react"; import { Link, PrimaryButton, Stack, Text } from "@fluentui/react";
import { DatabaseAccount } from "Contracts/DataModels"; import { DatabaseAccount } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils"; import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import LoadingOverlay from "../../../../../Common/LoadingOverlay"; import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import { logError } from "../../../../../Common/Logger"; import { logError } from "../../../../../Common/Logger";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import { buildResourceLink, getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import { AccountValidatorFn } from "../../../Types/CopyJobTypes"; import { AccountValidatorFn } from "../../../Types/CopyJobTypes";
@@ -12,14 +12,14 @@ import InfoTooltip from "../Components/InfoTooltip";
const tooltipContent = ( const tooltipContent = (
<Text> <Text>
{ContainerCopyMessages.pointInTimeRestore.tooltip.content} &nbsp; {t(Keys.containerCopy.pointInTimeRestore.tooltipContent)} &nbsp;
<Link <Link
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.pointInTimeRestore.tooltip.href} href={t(Keys.containerCopy.pointInTimeRestore.tooltipHref)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText} {t(Keys.containerCopy.pointInTimeRestore.tooltipHrefText)}
</Link> </Link>
</Text> </Text>
); );
@@ -119,9 +119,9 @@ const PointInTimeRestore: React.FC = () => {
return ( return (
<Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}> <Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<LoadingOverlay isLoading={loading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} /> <LoadingOverlay isLoading={loading} label={t(Keys.containerCopy.popoverOverlaySpinnerLabel)} />
<Stack.Item className="toggle-label"> <Stack.Item className="toggle-label">
{ContainerCopyMessages.pointInTimeRestore.description(source.account?.name ?? "")} {t(Keys.containerCopy.pointInTimeRestore.description, { accessName: source.account?.name ?? "" })}
{tooltipContent && ( {tooltipContent && (
<> <>
{" "} {" "}
@@ -134,7 +134,7 @@ const PointInTimeRestore: React.FC = () => {
<PrimaryButton <PrimaryButton
data-test="pointInTimeRestore:RefreshBtn" data-test="pointInTimeRestore:RefreshBtn"
className="fullWidth" className="fullWidth"
text={ContainerCopyMessages.refreshButtonLabel} text={t(Keys.common.refresh)}
iconProps={{ iconName: "Refresh" }} iconProps={{ iconName: "Refresh" }}
onClick={handleRefresh} onClick={handleRefresh}
/> />
@@ -142,7 +142,7 @@ const PointInTimeRestore: React.FC = () => {
<PrimaryButton <PrimaryButton
data-test="pointInTimeRestore:PrimaryBtn" data-test="pointInTimeRestore:PrimaryBtn"
className="fullWidth" className="fullWidth"
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText} text={loading ? "" : t(Keys.containerCopy.pointInTimeRestore.buttonText)}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})} {...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}
disabled={loading} disabled={loading}
onClick={openWindowAndMonitor} onClick={openWindowAndMonitor}
@@ -7,7 +7,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
<span <span
class="themeText css-110" class="themeText css-110"
> >
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 dont 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 <a
class="ms-Link root-111" class="ms-Link root-111"
@@ -95,7 +95,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
<span <span
class="themeText css-110" class="themeText css-110"
> >
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 dont 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 <a
class="ms-Link root-111" class="ms-Link root-111"
@@ -204,7 +204,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
<span <span
class="themeText css-110" class="themeText css-110"
> >
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.
</span> </span>
<div <div
class="ms-Stack css-125" class="ms-Stack css-125"
@@ -267,7 +267,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
<span <span
class="themeText css-110" class="themeText css-110"
> >
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 dont 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 <a
class="ms-Link root-111" class="ms-Link root-111"
@@ -359,7 +359,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
<span <span
class="themeText css-110" class="themeText css-110"
> >
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.
</span> </span>
<div <div
class="ms-Stack css-125" class="ms-Stack css-125"
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -8,7 +8,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -24,7 +24,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -63,7 +63,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -71,7 +71,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -87,7 +87,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -126,7 +126,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when sourceReadAccessFromTarget is true 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when sourceReadWriteAccessFromTarget is true 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -134,7 +134,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -150,7 +150,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -189,7 +189,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -197,7 +197,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -213,7 +213,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -255,12 +255,12 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<div <div
data-testid="popover-title" data-testid="popover-title"
> >
Read permissions assigned to default identity. Assign read-write permissions to default identity.
</div> </div>
<div <div
data-testid="popover-content" data-testid="popover-content"
> >
Assign read permissions of the source account to the default identity of the destination account. To confirm click the Yes button. Assign read-write permissions on the destination account to the default identity of the source account. To confirm, click the "Yes" button.
</div> </div>
<button <button
data-testid="popover-cancel" data-testid="popover-cancel"
@@ -277,7 +277,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -285,7 +285,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -301,7 +301,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -340,7 +340,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div> </div>
`; `;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = ` exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
<div> <div>
<div <div
class="ms-Stack defaultManagedIdentityContainer css-109" class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -348,7 +348,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span <span
class="toggle-label css-110" class="toggle-label css-110"
> >
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
@@ -364,7 +364,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Read permissions. Read-write permissions.
</a> </a>
</span> </span>
</div> </div>
@@ -9,7 +9,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
class="ms-Stack css-111" class="ms-Stack css-111"
@@ -212,7 +212,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
class="ms-Stack css-111" class="ms-Stack css-111"
@@ -618,7 +618,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
class="ms-Stack css-111" class="ms-Stack css-111"
@@ -1153,7 +1153,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
class="ms-Stack css-111" class="ms-Stack css-111"
@@ -1307,7 +1307,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
data-testid="shimmer-tree" data-testid="shimmer-tree"
@@ -1329,7 +1329,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<span <span
class="css-110" class="css-110"
> >
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps. To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.
</span> </span>
<div <div
data-testid="shimmer-tree" data-testid="shimmer-tree"
@@ -9,7 +9,8 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "" by switching it on. Set the system-assigned managed identity as default for "" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -71,8 +72,9 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
<div <div
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "undefined" by switching it on. Set the system-assigned managed identity as default for "" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -135,7 +137,8 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on. Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -227,7 +230,8 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on. Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -290,7 +294,8 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
class="toggle-label" class="toggle-label"
> >
Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on. Set the system-assigned managed identity as default for "test-cosmos-account" by switching it on.
 
 
<div <div
data-testid="info-tooltip" data-testid="info-tooltip"
> >
@@ -26,18 +26,18 @@ const useManagedIdentity = (
const handleAddSystemIdentity = useCallback(async (): Promise<void> => { const handleAddSystemIdentity = useCallback(async (): Promise<void> => {
try { try {
setLoading(true); setLoading(true);
const selectedTargetAccount = copyJobState?.target?.account; const selectedSourceAccount = copyJobState?.source?.account;
const { const {
subscriptionId: targetSubscriptionId, subscriptionId: sourceSubscriptionId,
resourceGroup: targetResourceGroup, resourceGroup: sourceResourceGroup,
accountName: targetAccountName, accountName: sourceAccountName,
} = getAccountDetailsFromResourceId(selectedTargetAccount?.id) || {}; } = getAccountDetailsFromResourceId(selectedSourceAccount?.id) || {};
const updatedAccount = await updateIdentityFn(targetSubscriptionId, targetResourceGroup, targetAccountName); const updatedAccount = await updateIdentityFn(sourceSubscriptionId, sourceResourceGroup, sourceAccountName);
if (updatedAccount) { if (updatedAccount) {
setCopyJobState((prevState) => ({ setCopyJobState((prevState) => ({
...prevState, ...prevState,
target: { ...prevState.target, account: updatedAccount }, source: { ...prevState.source, account: updatedAccount },
})); }));
} }
} catch (error) { } catch (error) {
@@ -46,7 +46,7 @@ const useManagedIdentity = (
setContextError(errorMessage); setContextError(errorMessage);
setLoading(false); setLoading(false);
} }
}, [copyJobState?.target?.account?.id, updateIdentityFn, setCopyJobState]); }, [copyJobState?.source?.account?.id, updateIdentityFn, setCopyJobState]);
return { loading, handleAddSystemIdentity }; return { loading, handleAddSystemIdentity };
}; };
@@ -13,7 +13,7 @@ import {
import { CopyJobContextState } from "../../../../Types/CopyJobTypes"; import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache"; import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache";
import usePermissionSections, { import usePermissionSections, {
checkTargetHasReaderRoleOnSource, checkTargetHasReadWriteRoleOnSource,
PermissionGroupConfig, PermissionGroupConfig,
SECTION_IDS, SECTION_IDS,
} from "./usePermissionsSection"; } from "./usePermissionsSection";
@@ -40,12 +40,12 @@ jest.mock("../AddManagedIdentity", () => {
return MockAddManagedIdentity; return MockAddManagedIdentity;
}); });
jest.mock("../AddReadPermissionToDefaultIdentity", () => { jest.mock("../AddReadWritePermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => { const MockAddReadWritePermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">AddReadPermissionToDefaultIdentity</div>; return <div data-testid="add-read-write-permission">AddReadWritePermissionToDefaultIdentity</div>;
}; };
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity"; MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity; return MockAddReadWritePermissionToDefaultIdentity;
}); });
jest.mock("../DefaultManagedIdentity", () => { jest.mock("../DefaultManagedIdentity", () => {
@@ -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: "",
}, },
@@ -193,7 +193,7 @@ describe("usePermissionsSection", () => {
expect(capturedResult[0].sections.map((s) => s.id)).toEqual([ expect(capturedResult[0].sections.map((s) => s.id)).toEqual([
SECTION_IDS.addManagedIdentity, SECTION_IDS.addManagedIdentity,
SECTION_IDS.defaultManagedIdentity, SECTION_IDS.defaultManagedIdentity,
SECTION_IDS.readPermissionAssigned, SECTION_IDS.readWritePermissionAssigned,
]); ]);
}); });
@@ -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: "",
}, },
@@ -284,16 +284,19 @@ describe("usePermissionsSection", () => {
describe("Section validation", () => { describe("Section validation", () => {
it("should validate addManagedIdentity section correctly", async () => { it("should validate addManagedIdentity section correctly", async () => {
const stateWithSystemAssigned = createMockState({ const stateWithSystemAssigned = createMockState({
target: { source: {
account: { account: {
id: "target-account-id", id: "source-account-id",
name: "target-account", name: "source-account",
identity: { identity: {
type: IdentityType.SystemAssigned, type: IdentityType.SystemAssigned,
principalId: "principal-123", principalId: "principal-123",
}, },
properties: { properties: {
defaultIdentity: DefaultIdentityType.FirstPartyIdentity, backupPolicy: {
type: BackupPolicyType.Periodic,
},
capabilities: [],
}, },
location: "", location: "",
type: "", type: "",
@@ -322,16 +325,20 @@ describe("usePermissionsSection", () => {
it("should validate defaultManagedIdentity section correctly", async () => { it("should validate defaultManagedIdentity section correctly", async () => {
const stateWithSystemAssignedIdentity = createMockState({ const stateWithSystemAssignedIdentity = createMockState({
target: { source: {
account: { account: {
id: "target-account-id", id: "source-account-id",
name: "target-account", name: "source-account",
identity: { identity: {
type: IdentityType.SystemAssigned, type: IdentityType.SystemAssigned,
principalId: "principal-123", principalId: "principal-123",
}, },
properties: { properties: {
defaultIdentity: DefaultIdentityType.SystemAssignedIdentity, defaultIdentity: DefaultIdentityType.SystemAssignedIdentity,
backupPolicy: {
type: BackupPolicyType.Periodic,
},
capabilities: [],
}, },
location: "", location: "",
type: "", type: "",
@@ -358,16 +365,17 @@ describe("usePermissionsSection", () => {
expect(defaultManagedIdentitySection?.completed).toBe(true); expect(defaultManagedIdentitySection?.completed).toBe(true);
}); });
it("should validate readPermissionAssigned section with reader role", async () => { it("should validate readWritePermissionAssigned section with contributor role", async () => {
const mockRoleDefinitions: RbacUtils.RoleDefinitionType[] = [ const mockRoleDefinitions: RbacUtils.RoleDefinitionType[] = [
{ {
id: "role-1", id: "role-1",
name: "Custom Role", name: "00000000-0000-0000-0000-000000000002",
permissions: [ permissions: [
{ {
dataActions: [ dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata", "Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read", "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
], ],
}, },
], ],
@@ -383,16 +391,20 @@ describe("usePermissionsSection", () => {
mockedRbacUtils.fetchRoleDefinitions.mockResolvedValue(mockRoleDefinitions); mockedRbacUtils.fetchRoleDefinitions.mockResolvedValue(mockRoleDefinitions);
const state = createMockState({ const state = createMockState({
target: { source: {
account: { account: {
id: "target-account-id", id: "source-account-id",
name: "target-account", name: "source-account",
identity: { identity: {
type: IdentityType.SystemAssigned, type: IdentityType.SystemAssigned,
principalId: "principal-123", principalId: "principal-123",
}, },
properties: { properties: {
defaultIdentity: DefaultIdentityType.SystemAssignedIdentity, defaultIdentity: DefaultIdentityType.SystemAssignedIdentity,
backupPolicy: {
type: BackupPolicyType.Periodic,
},
capabilities: [],
}, },
location: "", location: "",
type: "", type: "",
@@ -407,7 +419,9 @@ describe("usePermissionsSection", () => {
render(<TestWrapper state={state} onResult={noop} />); render(<TestWrapper state={state} onResult={noop} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.readPermissionAssigned}-completed`)).toHaveTextContent("true"); expect(screen.getByTestId(`section-${SECTION_IDS.readWritePermissionAssigned}-completed`)).toHaveTextContent(
"true",
);
}); });
expect(mockedRbacUtils.fetchRoleAssignments).toHaveBeenCalledWith( expect(mockedRbacUtils.fetchRoleAssignments).toHaveBeenCalledWith(
@@ -435,7 +449,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscription: undefined, subscriptionId: "",
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -476,7 +490,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscription: undefined, subscriptionId: "",
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -546,7 +560,7 @@ describe("usePermissionsSection", () => {
type: "", type: "",
kind: "", kind: "",
}, },
subscriptionId: "", subscription: undefined,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
@@ -568,12 +582,12 @@ describe("usePermissionsSection", () => {
}); });
}); });
describe("checkTargetHasReaderRoleOnSource", () => { describe("checkTargetHasReadWriteRoleOnSource", () => {
it("should return true for built-in Reader role", () => { it("should return true for built-in Contributor role", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [ const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{ {
id: "role-1", id: "role-1",
name: "00000000-0000-0000-0000-000000000001", name: "00000000-0000-0000-0000-000000000002",
permissions: [], permissions: [],
assignableScopes: [], assignableScopes: [],
resourceGroup: "", resourceGroup: "",
@@ -583,20 +597,21 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true); expect(result).toBe(true);
}); });
it("should return true for custom role with required data actions", () => { it("should return true for custom role with read-write data actions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [ const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{ {
id: "role-1", id: "role-1",
name: "Custom Reader Role", name: "Custom Contributor Role",
permissions: [ permissions: [
{ {
dataActions: [ dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata", "Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read", "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
], ],
}, },
], ],
@@ -608,7 +623,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true); expect(result).toBe(true);
}); });
@@ -630,12 +645,12 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("should return false for empty role definitions", () => { it("should return false for empty role definitions", () => {
const result = checkTargetHasReaderRoleOnSource([]); const result = checkTargetHasReadWriteRoleOnSource([]);
expect(result).toBe(false); expect(result).toBe(false);
}); });
@@ -653,11 +668,11 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("should handle multiple roles and return true if any has sufficient permissions", () => { it("should handle multiple roles and return true if any has sufficient read-write permissions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [ const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{ {
id: "role-1", id: "role-1",
@@ -675,7 +690,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
{ {
id: "role-2", id: "role-2",
name: "00000000-0000-0000-0000-000000000001", name: "00000000-0000-0000-0000-000000000002",
permissions: [], permissions: [],
assignableScopes: [], assignableScopes: [],
resourceGroup: "", resourceGroup: "",
@@ -685,7 +700,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
}, },
]; ];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions); const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true); expect(result).toBe(true);
}); });
}); });
@@ -1,7 +1,7 @@
import { Keys, t } from "Localization";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { CapabilityNames } from "../../../../../../Common/Constants"; import { CapabilityNames } from "../../../../../../Common/Constants";
import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils"; import { fetchRoleAssignments, fetchRoleDefinitions, RoleDefinitionType } from "../../../../../../Utils/arm/RbacUtils";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { getAccountDetailsFromResourceId, getContainerIdentifiers, isIntraAccountCopy } from "../../../../CopyJobUtils"; import { getAccountDetailsFromResourceId, getContainerIdentifiers, isIntraAccountCopy } from "../../../../CopyJobUtils";
import { import {
BackupPolicyType, BackupPolicyType,
@@ -12,7 +12,7 @@ import {
import { CopyJobContextState } from "../../../../Types/CopyJobTypes"; import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache"; import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
import AddManagedIdentity from "../AddManagedIdentity"; import AddManagedIdentity from "../AddManagedIdentity";
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity"; import AddReadWritePermissionToDefaultIdentity from "../AddReadWritePermissionToDefaultIdentity";
import DefaultManagedIdentity from "../DefaultManagedIdentity"; import DefaultManagedIdentity from "../DefaultManagedIdentity";
import OnlineCopyEnabled from "../OnlineCopyEnabled"; import OnlineCopyEnabled from "../OnlineCopyEnabled";
import PointInTimeRestore from "../PointInTimeRestore"; import PointInTimeRestore from "../PointInTimeRestore";
@@ -36,58 +36,60 @@ export interface PermissionGroupConfig {
export const SECTION_IDS = { export const SECTION_IDS = {
addManagedIdentity: "addManagedIdentity", addManagedIdentity: "addManagedIdentity",
defaultManagedIdentity: "defaultManagedIdentity", defaultManagedIdentity: "defaultManagedIdentity",
readPermissionAssigned: "readPermissionAssigned", readWritePermissionAssigned: "readWritePermissionAssigned",
pointInTimeRestore: "pointInTimeRestore", pointInTimeRestore: "pointInTimeRestore",
onlineCopyEnabled: "onlineCopyEnabled", onlineCopyEnabled: "onlineCopyEnabled",
} as const; } as const;
const COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID = "00000000-0000-0000-0000-000000000002";
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
{ {
id: SECTION_IDS.addManagedIdentity, id: SECTION_IDS.addManagedIdentity,
title: ContainerCopyMessages.addManagedIdentity.title, title: t(Keys.containerCopy.addManagedIdentity.title),
Component: AddManagedIdentity, Component: AddManagedIdentity,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
const targetAccountIdentityType = (state?.target?.account?.identity?.type ?? "").toLowerCase(); const sourceAccountIdentityType = (state?.source?.account?.identity?.type ?? "").toLowerCase();
return ( return (
targetAccountIdentityType === IdentityType.SystemAssigned || sourceAccountIdentityType === IdentityType.SystemAssigned ||
targetAccountIdentityType === IdentityType.UserAssigned sourceAccountIdentityType === IdentityType.UserAssigned
); );
}, },
}, },
{ {
id: SECTION_IDS.defaultManagedIdentity, id: SECTION_IDS.defaultManagedIdentity,
title: ContainerCopyMessages.defaultManagedIdentity.title, title: t(Keys.containerCopy.defaultManagedIdentity.title),
Component: DefaultManagedIdentity, Component: DefaultManagedIdentity,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
const targetAccountDefaultIdentity = (state?.target?.account?.properties?.defaultIdentity ?? "").toLowerCase(); const sourceAccountDefaultIdentity = (state?.source?.account?.properties?.defaultIdentity ?? "").toLowerCase();
return targetAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity; return sourceAccountDefaultIdentity === DefaultIdentityType.SystemAssignedIdentity;
}, },
}, },
{ {
id: SECTION_IDS.readPermissionAssigned, id: SECTION_IDS.readWritePermissionAssigned,
title: ContainerCopyMessages.readPermissionAssigned.title, title: t(Keys.containerCopy.readWritePermissionAssigned.title),
Component: AddReadPermissionToDefaultIdentity, Component: AddReadWritePermissionToDefaultIdentity,
disabled: true, disabled: true,
validate: async (state: CopyJobContextState) => { validate: async (state: CopyJobContextState) => {
const principalId = state?.target?.account?.identity?.principalId; const principalId = state?.source?.account?.identity?.principalId;
const selectedSourceAccount = state?.source?.account; const selectedTargetAccount = state?.target?.account;
const { const {
subscriptionId: sourceSubscriptionId, subscriptionId: targetSubscriptionId,
resourceGroup: sourceResourceGroup, resourceGroup: targetResourceGroup,
accountName: sourceAccountName, accountName: targetAccountName,
} = getAccountDetailsFromResourceId(selectedSourceAccount?.id); } = getAccountDetailsFromResourceId(selectedTargetAccount?.id);
const rolesAssigned = await fetchRoleAssignments( const rolesAssigned = await fetchRoleAssignments(
sourceSubscriptionId, targetSubscriptionId,
sourceResourceGroup, targetResourceGroup,
sourceAccountName, targetAccountName,
principalId, principalId,
); );
const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []); const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []);
return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []); return checkTargetHasReadWriteRoleOnSource(roleDefinitions ?? []);
}, },
}, },
]; ];
@@ -95,7 +97,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
{ {
id: SECTION_IDS.pointInTimeRestore, id: SECTION_IDS.pointInTimeRestore,
title: ContainerCopyMessages.pointInTimeRestore.title, title: t(Keys.containerCopy.pointInTimeRestore.title),
Component: PointInTimeRestore, Component: PointInTimeRestore,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
@@ -105,7 +107,7 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
}, },
{ {
id: SECTION_IDS.onlineCopyEnabled, id: SECTION_IDS.onlineCopyEnabled,
title: ContainerCopyMessages.onlineCopyEnabled.title, title: t(Keys.containerCopy.onlineCopyEnabled.title),
Component: OnlineCopyEnabled, Component: OnlineCopyEnabled,
disabled: true, disabled: true,
validate: (state: CopyJobContextState) => { validate: (state: CopyJobContextState) => {
@@ -119,18 +121,34 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
]; ];
/** /**
* Checks if the user has the Reader role based on role definitions. * Checks if the user has contributor-style read-write access on the source account.
*/ */
export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean { export function checkTargetHasReadWriteRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some( return roleDefinitions?.some((role) => {
(role) => if (role.name === COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID) {
role.name === "00000000-0000-0000-0000-000000000001" || return true;
role.permissions.some( }
(permission) =>
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata") && const dataActions = role.permissions?.flatMap((permission) => permission.dataActions ?? []) ?? [];
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read"),
), const hasAccountWildcard = dataActions.includes("Microsoft.DocumentDB/databaseAccounts/*");
); const hasContainerWildcard =
hasAccountWildcard || dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*");
const hasItemsWildcard =
hasContainerWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*");
const hasAccountReadMetadata =
hasAccountWildcard || dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata");
const hasItemRead =
hasItemsWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read");
const hasItemWrite =
hasItemsWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write");
return hasAccountReadMetadata && hasItemRead && hasItemWrite;
});
} }
/** /**
@@ -194,11 +212,11 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
if (crossAccountSections.length > 0) { if (crossAccountSections.length > 0) {
groups.push({ groups.push({
id: "crossAccountConfigs", id: "crossAccountConfigs",
title: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.title, title: t(Keys.containerCopy.assignPermissions.crossAccountConfiguration.title),
description: ContainerCopyMessages.assignPermissions.crossAccountConfiguration.description( description: t(Keys.containerCopy.assignPermissions.crossAccountConfiguration.description, {
sourceAccountName, sourceAccount: sourceAccountName,
targetAccountName, destinationAccount: targetAccountName,
), }),
sections: crossAccountSections, sections: crossAccountSections,
}); });
} }
@@ -206,8 +224,10 @@ const usePermissionSections = (state: CopyJobContextState): PermissionGroupConfi
if (state.migrationType === CopyJobMigrationType.Online) { if (state.migrationType === CopyJobMigrationType.Online) {
groups.push({ groups.push({
id: "onlineConfigs", id: "onlineConfigs",
title: ContainerCopyMessages.assignPermissions.onlineConfiguration.title, title: t(Keys.containerCopy.assignPermissions.onlineConfiguration.title),
description: ContainerCopyMessages.assignPermissions.onlineConfiguration.description(sourceAccountName), description: t(Keys.containerCopy.assignPermissions.onlineConfiguration.description, {
accountName: sourceAccountName,
}),
sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS], sections: [...PERMISSION_SECTIONS_FOR_ONLINE_JOBS],
}); });
} }
@@ -1,7 +1,7 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import PopoverMessage from "./PopoverContainer"; import PopoverMessage from "./PopoverContainer";
jest.mock("../../../../../Common/LoadingOverlay", () => { jest.mock("../../../../../Common/LoadingOverlay", () => {
@@ -181,7 +181,7 @@ describe("PopoverMessage Component", () => {
it("should use correct loading overlay label", () => { it("should use correct loading overlay label", () => {
render(<PopoverMessage {...defaultProps} isLoading={true} />); render(<PopoverMessage {...defaultProps} isLoading={true} />);
const loadingOverlay = screen.getByTestId("loading-overlay"); const loadingOverlay = screen.getByTestId("loading-overlay");
expect(loadingOverlay).toHaveAttribute("aria-label", ContainerCopyMessages.popoverOverlaySpinnerLabel); expect(loadingOverlay).toHaveAttribute("aria-label", t(Keys.containerCopy.popoverOverlaySpinnerLabel));
}); });
}); });
@@ -1,9 +1,9 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react"; import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import LoadingOverlay from "../../../../../Common/LoadingOverlay"; import LoadingOverlay from "../../../../../Common/LoadingOverlay";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
interface PopoverContainerProps { interface PopoverContainerProps {
isLoading?: boolean; isLoading?: boolean;
@@ -22,7 +22,7 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
tokens={{ childrenGap: 20 }} tokens={{ childrenGap: 20 }}
style={{ maxWidth: 450 }} style={{ maxWidth: 450 }}
> >
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} /> <LoadingOverlay isLoading={isLoading} label={t(Keys.containerCopy.popoverOverlaySpinnerLabel)} />
<Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}> <Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}>
{title} {title}
</Text> </Text>
@@ -4,8 +4,8 @@ import { CopyJobMigrationType } from "Explorer/ContainerCopy/Enums/CopyJobEnums"
import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes"; import { CopyJobContextProviderType } from "Explorer/ContainerCopy/Types/CopyJobTypes";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import AddCollectionPanelWrapper from "./AddCollectionPanelWrapper"; import AddCollectionPanelWrapper from "./AddCollectionPanelWrapper";
@@ -81,7 +81,7 @@ describe("AddCollectionPanelWrapper", () => {
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}, },
setCopyJobState: mockSetCopyJobState, setCopyJobState: mockSetCopyJobState,
flow: null, flow: null,
@@ -109,7 +109,9 @@ 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(t(Keys.containerCopy.selectContainers.createNewContainerSubHeadingDefault)),
).toBeInTheDocument();
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument(); expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
}); });
@@ -138,7 +140,7 @@ describe("AddCollectionPanelWrapper", () => {
it("should set header text to create container heading on mount", () => { it("should set header text to create container heading on mount", () => {
render(<AddCollectionPanelWrapper />); render(<AddCollectionPanelWrapper />);
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createContainerHeading); expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.selectContainers.createContainerHeading));
}); });
it("should reset header text to create copy job panel title on unmount", () => { it("should reset header text to create copy job panel title on unmount", () => {
@@ -146,13 +148,13 @@ describe("AddCollectionPanelWrapper", () => {
unmount(); unmount();
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createCopyJobPanelTitle); expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.createCopyJob.panelTitle));
}); });
it("should not change header text if already set correctly", () => { it("should not change header text if already set correctly", () => {
const modifiedSidePanelState = { const modifiedSidePanelState = {
...mockSidePanelState, ...mockSidePanelState,
headerText: ContainerCopyMessages.createContainerHeading, headerText: t(Keys.containerCopy.selectContainers.createContainerHeading),
}; };
mockUseSidePanel.getState = jest.fn().mockReturnValue(modifiedSidePanelState); mockUseSidePanel.getState = jest.fn().mockReturnValue(modifiedSidePanelState);
@@ -245,10 +247,10 @@ describe("AddCollectionPanelWrapper", () => {
describe("Component Lifecycle", () => { describe("Component Lifecycle", () => {
it("should properly cleanup on unmount", () => { it("should properly cleanup on unmount", () => {
const { unmount } = render(<AddCollectionPanelWrapper />); const { unmount } = render(<AddCollectionPanelWrapper />);
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createContainerHeading); expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.selectContainers.createContainerHeading));
mockSetHeaderText.mockClear(); mockSetHeaderText.mockClear();
unmount(); unmount();
expect(mockSetHeaderText).toHaveBeenCalledWith(ContainerCopyMessages.createCopyJobPanelTitle); expect(mockSetHeaderText).toHaveBeenCalledWith(t(Keys.containerCopy.createCopyJob.panelTitle));
}); });
it("should re-render correctly when props change", () => { it("should re-render correctly when props change", () => {
@@ -1,11 +1,14 @@
import { Stack, Text } from "@fluentui/react"; import { IDropdownOption, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { readDatabasesWithARM } 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 { Keys, t } from "Localization";
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 { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
type AddCollectionPanelWrapperProps = { type AddCollectionPanelWrapperProps = {
explorer?: Explorer; explorer?: Explorer;
@@ -13,18 +16,88 @@ 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,
capabilities: copyJobState?.target?.account?.properties?.capabilities ?? [],
capacityMode: copyJobState?.target?.account?.properties?.capacityMode,
enableFreeTier: copyJobState?.target?.account?.properties?.enableFreeTier,
enableAnalyticalStorage: copyJobState?.target?.account?.properties?.enableAnalyticalStorage,
};
}, [copyJobState?.target?.account?.id]);
useEffect(() => { useEffect(() => {
const sidePanelStore = useSidePanel.getState(); const sidePanelStore = useSidePanel.getState();
if (sidePanelStore.headerText !== ContainerCopyMessages.createContainerHeading) { if (sidePanelStore.headerText !== t(Keys.containerCopy.selectContainers.createContainerHeading)) {
sidePanelStore.setHeaderText(ContainerCopyMessages.createContainerHeading); sidePanelStore.setHeaderText(t(Keys.containerCopy.selectContainers.createContainerHeading));
} }
return () => { return () => {
sidePanelStore.setHeaderText(ContainerCopyMessages.createCopyJobPanelTitle); sidePanelStore.setHeaderText(t(Keys.containerCopy.createCopyJob.panelTitle));
}; };
}, []); }, []);
useEffect(() => {
if (!targetAccountOverride) {
setIsLoading(false);
return undefined;
}
let cancelled = false;
const fetchDatabases = async () => {
setIsLoading(true);
setPermissionError(null);
try {
const databases = await readDatabasesWithARM({
subscriptionId: targetAccountOverride.subscriptionId,
resourceGroup: targetAccountOverride.resourceGroup,
accountName: targetAccountOverride.accountName,
apiType: "SQL",
});
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 +111,41 @@ 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">
{targetAccountOverride?.accountName
? t(Keys.containerCopy.selectContainers.createNewContainerSubHeading, {
accountName: targetAccountOverride.accountName,
})
: t(Keys.containerCopy.selectContainers.createNewContainerSubHeadingDefault)}
</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,18 +87,18 @@ 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",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
...overrides, ...overrides,
}; };
@@ -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,12 +285,12 @@ 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",
}, },
sourceReadAccessFromTarget: true, sourceReadWriteAccessFromTarget: true,
}); });
const { container } = render( const { container } = render(
@@ -350,7 +350,7 @@ describe("PreviewCopyJob", () => {
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
}); });
it("should display proper field labels from ContainerCopyMessages", () => { it("should display proper field labels", () => {
const mockContext = createMockContext(); const mockContext = createMockContext();
const { getByText } = render( const { getByText } = render(
@@ -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();
}); });
}); });
@@ -1,6 +1,6 @@
import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from "@fluentui/react"; import { DetailsList, DetailsListLayoutMode, Stack, Text, TextField } from "@fluentui/react";
import { Keys, t } from "Localization";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getDefaultJobName } from "../../../CopyJobUtils"; import { getDefaultJobName } from "../../../CopyJobUtils";
import FieldRow from "../Components/FieldRow"; import FieldRow from "../Components/FieldRow";
@@ -32,19 +32,19 @@ const PreviewCopyJob: React.FC = () => {
}; };
return ( return (
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob"> <Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
<FieldRow label={ContainerCopyMessages.jobNameLabel}> <FieldRow label={t(Keys.containerCopy.preview.jobNameLabel)}>
<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">{t(Keys.containerCopy.preview.subscriptionLabel)}</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">{t(Keys.containerCopy.preview.accountLabel)}</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>
@@ -1,5 +1,5 @@
import { IColumn } from "@fluentui/react"; import { IColumn } from "@fluentui/react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages"; import { Keys, t } from "Localization";
const commonProps = { const commonProps = {
minWidth: 130, minWidth: 130,
@@ -17,25 +17,25 @@ export const getPreviewCopyJobDetailsListColumns = (): IColumn[] => {
return [ return [
{ {
key: "sourcedbname", key: "sourcedbname",
name: ContainerCopyMessages.sourceDatabaseLabel, name: t(Keys.containerCopy.preview.sourceDatabaseLabel),
fieldName: "sourceDatabaseName", fieldName: "sourceDatabaseName",
...commonProps, ...commonProps,
}, },
{ {
key: "sourcecolname", key: "sourcecolname",
name: ContainerCopyMessages.sourceContainerLabel, name: t(Keys.containerCopy.preview.sourceContainerLabel),
fieldName: "sourceContainerName", fieldName: "sourceContainerName",
...commonProps, ...commonProps,
}, },
{ {
key: "targetdbname", key: "targetdbname",
name: ContainerCopyMessages.targetDatabaseLabel, name: t(Keys.containerCopy.preview.targetDatabaseLabel),
fieldName: "targetDatabaseName", fieldName: "targetDatabaseName",
...commonProps, ...commonProps,
}, },
{ {
key: "targetcolname", key: "targetcolname",
name: ContainerCopyMessages.targetContainerLabel, name: t(Keys.containerCopy.preview.targetContainerLabel),
fieldName: "targetContainerName", fieldName: "targetContainerName",
...commonProps, ...commonProps,
}, },
@@ -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>
@@ -1,11 +1,11 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { configContext, Platform } from "../../../../../../ConfigContext"; import { configContext, Platform } from "../../../../../../ConfigContext";
import { DatabaseAccount } from "../../../../../../Contracts/DataModels"; import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
import * as useDatabaseAccountsHook from "../../../../../../hooks/useDatabaseAccounts"; import * as useDatabaseAccountsHook from "../../../../../../hooks/useDatabaseAccounts";
import { apiType, userContext } from "../../../../../../UserContext"; import { apiType, userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../../Context/CopyJobContext"; import { CopyJobContext } from "../../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes"; import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
@@ -38,6 +38,12 @@ describe("AccountDropdown", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
source: { source: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
target: {
subscription: { subscription: {
subscriptionId: "test-subscription-id", subscriptionId: "test-subscription-id",
displayName: "Test Subscription", displayName: "Test Subscription",
@@ -46,13 +52,7 @@ describe("AccountDropdown", () => {
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
target: { sourceReadWriteAccessFromTarget: false,
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState; } as CopyJobContextState;
const mockCopyJobContextValue = { const mockCopyJobContextValue = {
@@ -129,11 +129,11 @@ describe("AccountDropdown", () => {
renderWithContext(); renderWithContext();
expect( expect(
screen.getByText(`${ContainerCopyMessages.sourceAccountDropdownLabel}:`, { exact: true }), screen.getByText(`${t(Keys.containerCopy.selectAccount.accountDropdownLabel)}:`, { exact: true }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("combobox")).toHaveAttribute( expect(screen.getByRole("combobox")).toHaveAttribute(
"aria-label", "aria-label",
ContainerCopyMessages.sourceAccountDropdownLabel, t(Keys.containerCopy.selectAccount.accountDropdownLabel),
); );
}); });
@@ -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", t(Keys.containerCopy.selectAccount.accountDropdownLabel));
}); });
it("should have required attribute", () => { it("should have required attribute", () => {
@@ -2,11 +2,11 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react"; import { Dropdown } from "@fluentui/react";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import { Keys, t } from "Localization";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { DatabaseAccount } from "../../../../../../Contracts/DataModels"; import { DatabaseAccount } from "../../../../../../Contracts/DataModels";
import { useDatabaseAccounts } from "../../../../../../hooks/useDatabaseAccounts"; import { useDatabaseAccounts } from "../../../../../../hooks/useDatabaseAccounts";
import { apiType, userContext } from "../../../../../../UserContext"; import { apiType, userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
@@ -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={t(Keys.containerCopy.selectAccount.accountDropdownLabel)}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder} placeholder={t(Keys.containerCopy.selectAccount.accountDropdownPlaceholder)}
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel} ariaLabel={t(Keys.containerCopy.selectAccount.accountDropdownLabel)}
options={accountOptions} options={accountOptions}
disabled={isAccountDropdownDisabled} disabled={isAccountDropdownDisabled}
required required
@@ -1,7 +1,7 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { MigrationType } from "./MigrationType"; import { MigrationType } from "./MigrationType";
@@ -29,7 +29,7 @@ describe("MigrationType", () => {
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}, },
setCopyJobState: mockSetCopyJobState, setCopyJobState: mockSetCopyJobState,
flow: { currentScreen: "selectAccount" }, flow: { currentScreen: "selectAccount" },
@@ -53,9 +53,9 @@ describe("MigrationType", () => {
expect(screen.getByRole("radiogroup")).toBeInTheDocument(); expect(screen.getByRole("radiogroup")).toBeInTheDocument();
const offlineRadio = screen.getByRole("radio", { const offlineRadio = screen.getByRole("radio", {
name: ContainerCopyMessages.migrationTypeOptions.offline.title, name: t(Keys.containerCopy.migrationType.offline.title),
}); });
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }); const onlineRadio = screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) });
expect(offlineRadio).toBeInTheDocument(); expect(offlineRadio).toBeInTheDocument();
expect(onlineRadio).toBeInTheDocument(); expect(onlineRadio).toBeInTheDocument();
@@ -65,9 +65,9 @@ describe("MigrationType", () => {
it("should render with online mode selected by default", () => { it("should render with online mode selected by default", () => {
render(<MigrationType />); render(<MigrationType />);
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }); const onlineRadio = screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) });
const offlineRadio = screen.getByRole("radio", { const offlineRadio = screen.getByRole("radio", {
name: ContainerCopyMessages.migrationTypeOptions.offline.title, name: t(Keys.containerCopy.migrationType.offline.title),
}); });
expect(onlineRadio).toBeChecked(); expect(onlineRadio).toBeChecked();
@@ -86,9 +86,9 @@ describe("MigrationType", () => {
render(<MigrationType />); render(<MigrationType />);
const offlineRadio = screen.getByRole("radio", { const offlineRadio = screen.getByRole("radio", {
name: ContainerCopyMessages.migrationTypeOptions.offline.title, name: t(Keys.containerCopy.migrationType.offline.title),
}); });
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }); const onlineRadio = screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) });
expect(offlineRadio).toBeChecked(); expect(offlineRadio).toBeChecked();
expect(onlineRadio).not.toBeChecked(); expect(onlineRadio).not.toBeChecked();
@@ -141,7 +141,7 @@ describe("MigrationType", () => {
render(<MigrationType />); render(<MigrationType />);
const offlineRadio = screen.getByRole("radio", { const offlineRadio = screen.getByRole("radio", {
name: ContainerCopyMessages.migrationTypeOptions.offline.title, name: t(Keys.containerCopy.migrationType.offline.title),
}); });
fireEvent.click(offlineRadio); fireEvent.click(offlineRadio);
@@ -167,7 +167,7 @@ describe("MigrationType", () => {
render(<MigrationType />); render(<MigrationType />);
const onlineRadio = screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }); const onlineRadio = screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) });
fireEvent.click(onlineRadio); fireEvent.click(onlineRadio);
expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function)); expect(mockSetCopyJobState).toHaveBeenCalledWith(expect.any(Function));
@@ -198,11 +198,9 @@ describe("MigrationType", () => {
render(<MigrationType />); render(<MigrationType />);
expect( expect(
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }), screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.offline.title) }),
).toBeInTheDocument();
expect(
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) })).toBeInTheDocument();
}); });
}); });
@@ -220,11 +218,9 @@ describe("MigrationType", () => {
expect(container.querySelector("[data-test='migration-type']")).toBeInTheDocument(); expect(container.querySelector("[data-test='migration-type']")).toBeInTheDocument();
expect( expect(
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.offline.title }), screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.offline.title) }),
).toBeInTheDocument();
expect(
screen.getByRole("radio", { name: ContainerCopyMessages.migrationTypeOptions.online.title }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("radio", { name: t(Keys.containerCopy.migrationType.online.title) })).toBeInTheDocument();
}); });
it("should handle null copyJobState gracefully", () => { it("should handle null copyJobState gracefully", () => {
@@ -3,20 +3,20 @@
import { ChoiceGroup, IChoiceGroupOption, Stack, Text } from "@fluentui/react"; import { ChoiceGroup, IChoiceGroupOption, Stack, Text } from "@fluentui/react";
import MarkdownRender from "@nteract/markdown"; import MarkdownRender from "@nteract/markdown";
import { useCopyJobContext } from "Explorer/ContainerCopy/Context/CopyJobContext"; import { useCopyJobContext } from "Explorer/ContainerCopy/Context/CopyJobContext";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums"; import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
interface MigrationTypeProps {} interface MigrationTypeProps {}
const options: IChoiceGroupOption[] = [ const options: IChoiceGroupOption[] = [
{ {
key: CopyJobMigrationType.Offline, key: CopyJobMigrationType.Offline,
text: ContainerCopyMessages.migrationTypeOptions.offline.title, text: t(Keys.containerCopy.migrationType.offline.title),
styles: { root: { width: "33%" } }, styles: { root: { width: "33%" } },
}, },
{ {
key: CopyJobMigrationType.Online, key: CopyJobMigrationType.Online,
text: ContainerCopyMessages.migrationTypeOptions.online.title, text: t(Keys.containerCopy.migrationType.online.title),
styles: { root: { width: "33%" } }, styles: { root: { width: "33%" } },
}, },
]; ];
@@ -47,8 +47,13 @@ export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
}; };
const selectedKey = copyJobState?.migrationType ?? ""; const selectedKey = copyJobState?.migrationType ?? "";
const selectedKeyLowercase = selectedKey.toLowerCase() as keyof typeof ContainerCopyMessages.migrationTypeOptions; const selectedKeyLowercase = selectedKey.toLowerCase() as "offline" | "online";
const selectedKeyContent = ContainerCopyMessages.migrationTypeOptions[selectedKeyLowercase]; const migrationTypeDescriptionKey =
selectedKeyLowercase === "offline"
? Keys.containerCopy.migrationType.offline.description
: selectedKeyLowercase === "online"
? Keys.containerCopy.migrationType.online.description
: null;
return ( return (
<Stack data-test="migration-type" className="migrationTypeContainer"> <Stack data-test="migration-type" className="migrationTypeContainer">
@@ -61,14 +66,14 @@ export const MigrationType: React.FC<MigrationTypeProps> = React.memo(() => {
styles={choiceGroupStyles} styles={choiceGroupStyles}
/> />
</Stack.Item> </Stack.Item>
{selectedKeyContent && ( {migrationTypeDescriptionKey && (
<Stack.Item styles={{ root: { marginTop: 10 } }}> <Stack.Item styles={{ root: { marginTop: 10 } }}>
<Text <Text
variant="small" variant="small"
className="migrationTypeDescription" className="migrationTypeDescription"
data-test={`migration-type-description-${selectedKeyLowercase}`} data-test={`migration-type-description-${selectedKeyLowercase}`}
> >
<MarkdownRender source={selectedKeyContent.description} linkTarget="_blank" /> <MarkdownRender source={t(migrationTypeDescriptionKey)} linkTarget="_blank" />
</Text> </Text>
</Stack.Item> </Stack.Item>
)} )}
@@ -8,14 +8,9 @@ import { SubscriptionDropdown } from "./SubscriptionDropdown";
jest.mock("../../../../../../hooks/useSubscriptions"); jest.mock("../../../../../../hooks/useSubscriptions");
jest.mock("../../../../../../UserContext"); jest.mock("../../../../../../UserContext");
jest.mock("../../../../ContainerCopyMessages");
const mockUseSubscriptions = jest.requireMock("../../../../../../hooks/useSubscriptions").useSubscriptions; const mockUseSubscriptions = jest.requireMock("../../../../../../hooks/useSubscriptions").useSubscriptions;
const mockUserContext = jest.requireMock("../../../../../../UserContext").userContext; const mockUserContext = jest.requireMock("../../../../../../UserContext").userContext;
const mockContainerCopyMessages = jest.requireMock("../../../../ContainerCopyMessages").default;
mockContainerCopyMessages.subscriptionDropdownLabel = "Subscription";
mockContainerCopyMessages.subscriptionDropdownPlaceholder = "Select a subscription";
describe("SubscriptionDropdown", () => { describe("SubscriptionDropdown", () => {
let mockExplorer: Explorer; let mockExplorer: Explorer;
@@ -1,11 +1,11 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { Dropdown } from "@fluentui/react"; import { Dropdown } from "@fluentui/react";
import { Keys, t } from "Localization";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Subscription } from "../../../../../../Contracts/DataModels"; import { Subscription } from "../../../../../../Contracts/DataModels";
import { useSubscriptions } from "../../../../../../hooks/useSubscriptions"; import { useSubscriptions } from "../../../../../../hooks/useSubscriptions";
import { userContext } from "../../../../../../UserContext"; import { userContext } from "../../../../../../UserContext";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../../Context/CopyJobContext";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
@@ -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,13 +61,13 @@ 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={t(Keys.containerCopy.selectAccount.subscriptionDropdownLabel)}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.subscriptionDropdownPlaceholder} placeholder={t(Keys.containerCopy.selectAccount.subscriptionDropdownPlaceholder)}
ariaLabel={ContainerCopyMessages.subscriptionDropdownLabel} ariaLabel={t(Keys.containerCopy.selectAccount.subscriptionDropdownLabel)}
data-test="subscription-dropdown" data-test="subscription-dropdown"
options={subscriptionOptions} options={subscriptionOptions}
required required
@@ -30,18 +30,18 @@ describe("SelectAccount", () => {
jobName: "", jobName: "",
migrationType: CopyJobMigrationType.Online, migrationType: CopyJobMigrationType.Online,
source: { source: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "", subscriptionId: "",
account: null as any, account: null as any,
databaseId: "", databaseId: "",
containerId: "", containerId: "",
}, },
sourceReadAccessFromTarget: false, target: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
}, },
setCopyJobState: mockSetCopyJobState, setCopyJobState: mockSetCopyJobState,
flow: { currentScreen: "selectAccount" }, flow: { currentScreen: "selectAccount" },
@@ -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();
@@ -1,6 +1,6 @@
import { Stack, Text } from "@fluentui/react"; import { Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { AccountDropdown } from "./Components/AccountDropdown"; import { AccountDropdown } from "./Components/AccountDropdown";
import { MigrationType } from "./Components/MigrationType"; import { MigrationType } from "./Components/MigrationType";
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown"; import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
@@ -8,7 +8,7 @@ import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
const SelectAccount = React.memo(() => { const SelectAccount = React.memo(() => {
return ( return (
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}> <Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
<Text className="themeText">{ContainerCopyMessages.selectAccountDescription}</Text> <Text className="themeText">{t(Keys.containerCopy.selectAccount.description)}</Text>
<SubscriptionDropdown /> <SubscriptionDropdown />
@@ -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"
@@ -7,19 +7,9 @@ import { dropDownChangeHandler } from "./DropDownChangeHandler";
const createMockInitialState = (): CopyJobContextState => ({ const createMockInitialState = (): CopyJobContextState => ({
jobName: "test-job", jobName: "test-job",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: 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);
}); });
@@ -181,7 +181,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.jobName).toBe(initialState.jobName); expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType); expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget); expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
}); });
}); });
@@ -193,7 +193,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.source.containerId).toBe("new-source-container"); expect(capturedState.source.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);
}); });
@@ -227,7 +227,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.jobName).toBe(initialState.jobName); expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType); expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget); expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
}); });
}); });
@@ -239,7 +239,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.target.containerId).toBe("new-target-container"); expect(capturedState.target.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);
}); });
@@ -15,15 +15,6 @@ jest.mock("../../../../../hooks/useDataContainers", () => ({
useDataContainers: jest.fn(), useDataContainers: jest.fn(),
})); }));
jest.mock("../../../ContainerCopyMessages", () => ({
__esModule: true,
default: {
selectSourceAndTargetContainersDescription: "Select source and target containers for migration",
sourceContainerSubHeading: "Source Container",
targetContainerSubHeading: "Target Container",
},
}));
jest.mock("./Events/DropDownChangeHandler", () => ({ jest.mock("./Events/DropDownChangeHandler", () => ({
dropDownChangeHandler: jest.fn(() => () => jest.fn()), dropDownChangeHandler: jest.fn(() => () => jest.fn()),
})); }));
@@ -73,7 +64,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 +73,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",
@@ -90,7 +81,7 @@ describe("SelectSourceAndTargetContainers", () => {
databaseId: "db2", databaseId: "db2",
containerId: "container2", containerId: "container2",
}, },
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: false,
}; };
const mockMemoizedData = { const mockMemoizedData = {
@@ -124,22 +115,26 @@ describe("SelectSourceAndTargetContainers", () => {
describe("Component Rendering", () => { describe("Component Rendering", () => {
it("should render without crashing", () => { it("should render without crashing", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument(); expect(
screen.getByText("Please select a source container and a destination container to copy to."),
).toBeInTheDocument();
}); });
it("should render description text", () => { it("should render description text", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument(); expect(
screen.getByText("Please select a source container and a destination container to copy to."),
).toBeInTheDocument();
}); });
it("should render source container section", () => { it("should render source container section", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument(); expect(screen.getByText("Source container")).toBeInTheDocument();
}); });
it("should render target container section", () => { it("should render target container section", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
it("should return null when source is not available", () => { it("should return null when source is not available", () => {
@@ -238,14 +233,14 @@ describe("SelectSourceAndTargetContainers", () => {
describe("Component Props", () => { describe("Component Props", () => {
it("should pass showAddCollectionPanel to DatabaseContainerSection", () => { it("should pass showAddCollectionPanel to DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />); renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />);
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
it("should render without showAddCollectionPanel prop", () => { it("should render without showAddCollectionPanel prop", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument(); expect(screen.getByText("Source container")).toBeInTheDocument();
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
}); });
@@ -310,13 +305,13 @@ describe("SelectSourceAndTargetContainers", () => {
it("should pass correct props to source DatabaseContainerSection", () => { it("should pass correct props to source DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument(); expect(screen.getByText("Source container")).toBeInTheDocument();
}); });
it("should pass correct props to target DatabaseContainerSection", () => { it("should pass correct props to target DatabaseContainerSection", () => {
renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />); renderWithContext(<SelectSourceAndTargetContainers showAddCollectionPanel={mockShowAddCollectionPanel} />);
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
it("should disable source container dropdown when no database is selected", () => { it("should disable source container dropdown when no database is selected", () => {
@@ -329,7 +324,7 @@ describe("SelectSourceAndTargetContainers", () => {
} as ReturnType<typeof useSourceAndTargetData>); } as ReturnType<typeof useSourceAndTargetData>);
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Source Container")).toBeInTheDocument(); expect(screen.getByText("Source container")).toBeInTheDocument();
}); });
it("should disable target container dropdown when no database is selected", () => { it("should disable target container dropdown when no database is selected", () => {
@@ -342,7 +337,7 @@ describe("SelectSourceAndTargetContainers", () => {
} as ReturnType<typeof useSourceAndTargetData>); } as ReturnType<typeof useSourceAndTargetData>);
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Target Container")).toBeInTheDocument(); expect(screen.getByText("Destination container")).toBeInTheDocument();
}); });
}); });
@@ -353,7 +348,9 @@ describe("SelectSourceAndTargetContainers", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument(); expect(
screen.getByText("Please select a source container and a destination container to copy to."),
).toBeInTheDocument();
}); });
it("should handle hooks throwing errors gracefully", () => { it("should handle hooks throwing errors gracefully", () => {
@@ -421,7 +418,9 @@ describe("SelectSourceAndTargetContainers", () => {
it("should apply correct spacing tokens", () => { it("should apply correct spacing tokens", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
expect(screen.getByText("Select source and target containers for migration")).toBeInTheDocument(); expect(
screen.getByText("Please select a source container and a destination container to copy to."),
).toBeInTheDocument();
}); });
}); });
@@ -429,9 +428,9 @@ describe("SelectSourceAndTargetContainers", () => {
it("should render description, source section, and target section in correct order", () => { it("should render description, source section, and target section in correct order", () => {
renderWithContext(<SelectSourceAndTargetContainers />); renderWithContext(<SelectSourceAndTargetContainers />);
const description = screen.getByText("Select source and target containers for migration"); const description = screen.getByText("Please select a source container and a destination container to copy to.");
const sourceSection = screen.getByText("Source Container"); const sourceSection = screen.getByText("Source container");
const targetSection = screen.getByText("Target Container"); const targetSection = screen.getByText("Destination container");
expect(description).toBeInTheDocument(); expect(description).toBeInTheDocument();
expect(sourceSection).toBeInTheDocument(); expect(sourceSection).toBeInTheDocument();
@@ -1,9 +1,9 @@
import { Stack } from "@fluentui/react"; import { Stack } from "@fluentui/react";
import { DatabaseModel } from "Contracts/DataModels"; import { DatabaseModel } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { useDatabases } from "../../../../../hooks/useDatabases"; import { useDatabases } from "../../../../../hooks/useDatabases";
import { useDataContainers } from "../../../../../hooks/useDataContainers"; import { useDataContainers } from "../../../../../hooks/useDataContainers";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { DatabaseContainerSection } from "./components/DatabaseContainerSection"; import { DatabaseContainerSection } from "./components/DatabaseContainerSection";
import { dropDownChangeHandler } from "./Events/DropDownChangeHandler"; import { dropDownChangeHandler } from "./Events/DropDownChangeHandler";
@@ -52,9 +52,9 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
className="selectSourceAndTargetContainers" className="selectSourceAndTargetContainers"
tokens={{ childrenGap: 25 }} tokens={{ childrenGap: 25 }}
> >
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span> <span className="themeText">{t(Keys.containerCopy.selectContainers.description)}</span>
<DatabaseContainerSection <DatabaseContainerSection
heading={ContainerCopyMessages.sourceContainerSubHeading} heading={t(Keys.containerCopy.selectContainers.sourceContainerSubHeading)}
databaseOptions={sourceDatabaseOptions} databaseOptions={sourceDatabaseOptions}
selectedDatabase={source?.databaseId} selectedDatabase={source?.databaseId}
databaseDisabled={false} databaseDisabled={false}
@@ -66,7 +66,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
sectionType="source" sectionType="source"
/> />
<DatabaseContainerSection <DatabaseContainerSection
heading={ContainerCopyMessages.targetContainerSubHeading} heading={t(Keys.containerCopy.selectContainers.targetContainerSubHeading)}
databaseOptions={targetDatabaseOptions} databaseOptions={targetDatabaseOptions}
selectedDatabase={target?.databaseId} selectedDatabase={target?.databaseId}
databaseDisabled={false} databaseDisabled={false}
@@ -1,7 +1,7 @@
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DatabaseContainerSectionProps, DropdownOptionType } from "../../../../Types/CopyJobTypes"; import { DatabaseContainerSectionProps, DropdownOptionType } from "../../../../Types/CopyJobTypes";
import { DatabaseContainerSection } from "./DatabaseContainerSection"; import { DatabaseContainerSection } from "./DatabaseContainerSection";
@@ -60,11 +60,14 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
expect(databaseDropdown).toBeInTheDocument(); expect(databaseDropdown).toBeInTheDocument();
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel); expect(databaseDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
);
expect(databaseDropdown).not.toBeDisabled(); expect(databaseDropdown).not.toBeDisabled();
}); });
@@ -72,30 +75,35 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(containerDropdown).toBeInTheDocument(); expect(containerDropdown).toBeInTheDocument();
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel); expect(containerDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.containerDropdownLabel),
);
expect(containerDropdown).not.toBeDisabled(); expect(containerDropdown).not.toBeDisabled();
}); });
it("renders database label correctly", () => { it("renders database label correctly", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.databaseDropdownLabel}:`)).toBeInTheDocument(); expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}:`)).toBeInTheDocument();
}); });
it("renders container label correctly", () => { it("renders container label correctly", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.containerDropdownLabel}:`)).toBeInTheDocument(); expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.containerDropdownLabel)}:`)).toBeInTheDocument();
}); });
it("does not render create container button when handleOnDemandCreateContainer is not provided", () => { it("does not render create container button when handleOnDemandCreateContainer is not provided", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.queryByText(ContainerCopyMessages.createContainerButtonLabel)).not.toBeInTheDocument(); expect(
screen.queryByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel)),
).not.toBeInTheDocument();
}); });
it("renders create container button when handleOnDemandCreateContainer is provided", () => { it("renders create container button when handleOnDemandCreateContainer is provided", () => {
@@ -107,7 +115,7 @@ describe("DatabaseContainerSection", () => {
const createButton = container.querySelector(".create-container-link-btn"); const createButton = container.querySelector(".create-container-link-btn");
expect(createButton).toBeInTheDocument(); expect(createButton).toBeInTheDocument();
expect(createButton).toHaveTextContent(ContainerCopyMessages.createContainerButtonLabel); expect(createButton).toHaveTextContent(t(Keys.containerCopy.selectContainers.createContainerButtonLabel));
}); });
}); });
@@ -121,7 +129,7 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithDisabledDatabase} />); render(<DatabaseContainerSection {...propsWithDisabledDatabase} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
expect(databaseDropdown).toHaveAttribute("aria-disabled", "true"); expect(databaseDropdown).toHaveAttribute("aria-disabled", "true");
@@ -136,7 +144,7 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithDisabledContainer} />); render(<DatabaseContainerSection {...propsWithDisabledContainer} />);
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(containerDropdown).toHaveAttribute("aria-disabled", "true"); expect(containerDropdown).toHaveAttribute("aria-disabled", "true");
@@ -152,10 +160,10 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithFalsyDisabled} />); render(<DatabaseContainerSection {...propsWithFalsyDisabled} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(databaseDropdown).not.toHaveAttribute("aria-disabled", "true"); expect(databaseDropdown).not.toHaveAttribute("aria-disabled", "true");
@@ -167,21 +175,27 @@ describe("DatabaseContainerSection", () => {
it("calls databaseOnChange when database dropdown selection changes", () => { it("calls databaseOnChange when database dropdown selection changes", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
fireEvent.click(databaseDropdown); fireEvent.click(databaseDropdown);
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel); expect(databaseDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
);
}); });
it("calls containerOnChange when container dropdown selection changes", () => { it("calls containerOnChange when container dropdown selection changes", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
fireEvent.click(containerDropdown); fireEvent.click(containerDropdown);
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel); expect(containerDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.containerDropdownLabel),
);
}); });
it("calls handleOnDemandCreateContainer when create container button is clicked", () => { it("calls handleOnDemandCreateContainer when create container button is clicked", () => {
@@ -192,7 +206,7 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithCreateHandler} />); render(<DatabaseContainerSection {...propsWithCreateHandler} />);
const createButton = screen.getByText(ContainerCopyMessages.createContainerButtonLabel); const createButton = screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel));
fireEvent.click(createButton); fireEvent.click(createButton);
expect(mockHandleOnDemandCreateContainer).toHaveBeenCalledTimes(1); expect(mockHandleOnDemandCreateContainer).toHaveBeenCalledTimes(1);
@@ -235,10 +249,10 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithEmptyOptions} />); render(<DatabaseContainerSection {...propsWithEmptyOptions} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(databaseDropdown).toBeInTheDocument(); expect(databaseDropdown).toBeInTheDocument();
@@ -251,24 +265,30 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(databaseDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.databaseDropdownLabel); expect(databaseDropdown).toHaveAttribute(
expect(containerDropdown).toHaveAttribute("aria-label", ContainerCopyMessages.containerDropdownLabel); "aria-label",
t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
);
expect(containerDropdown).toHaveAttribute(
"aria-label",
t(Keys.containerCopy.selectContainers.containerDropdownLabel),
);
}); });
it("has proper required attributes for dropdowns", () => { it("has proper required attributes for dropdowns", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
const databaseDropdown = screen.getByRole("combobox", { const databaseDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.databaseDropdownLabel, name: t(Keys.containerCopy.selectContainers.databaseDropdownLabel),
}); });
const containerDropdown = screen.getByRole("combobox", { const containerDropdown = screen.getByRole("combobox", {
name: ContainerCopyMessages.containerDropdownLabel, name: t(Keys.containerCopy.selectContainers.containerDropdownLabel),
}); });
expect(databaseDropdown).toHaveAttribute("aria-required", "true"); expect(databaseDropdown).toHaveAttribute("aria-required", "true");
@@ -278,8 +298,8 @@ describe("DatabaseContainerSection", () => {
it("maintains proper label associations", () => { it("maintains proper label associations", () => {
render(<DatabaseContainerSection {...defaultProps} />); render(<DatabaseContainerSection {...defaultProps} />);
expect(screen.getByText(`${ContainerCopyMessages.databaseDropdownLabel}:`)).toBeInTheDocument(); expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}:`)).toBeInTheDocument();
expect(screen.getByText(`${ContainerCopyMessages.containerDropdownLabel}:`)).toBeInTheDocument(); expect(screen.getByText(`${t(Keys.containerCopy.selectContainers.containerDropdownLabel)}:`)).toBeInTheDocument();
}); });
}); });
@@ -299,7 +319,9 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...minimalProps} />); render(<DatabaseContainerSection {...minimalProps} />);
expect(screen.getByText("Test Heading")).toBeInTheDocument(); expect(screen.getByText("Test Heading")).toBeInTheDocument();
expect(screen.queryByText(ContainerCopyMessages.createContainerButtonLabel)).not.toBeInTheDocument(); expect(
screen.queryByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel)),
).not.toBeInTheDocument();
}); });
it("handles empty string selections", () => { it("handles empty string selections", () => {
@@ -366,7 +388,7 @@ describe("DatabaseContainerSection", () => {
const { container } = render(<DatabaseContainerSection {...propsWithCreateHandler} />); const { container } = render(<DatabaseContainerSection {...propsWithCreateHandler} />);
const createButton = screen.getByText(ContainerCopyMessages.createContainerButtonLabel); const createButton = screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel));
expect(createButton).toBeInTheDocument(); expect(createButton).toBeInTheDocument();
const containerSection = container.querySelector(".databaseContainerSection"); const containerSection = container.querySelector(".databaseContainerSection");
@@ -381,7 +403,7 @@ describe("DatabaseContainerSection", () => {
render(<DatabaseContainerSection {...propsWithCreateHandler} />); render(<DatabaseContainerSection {...propsWithCreateHandler} />);
expect(screen.getByText(ContainerCopyMessages.createContainerButtonLabel)).toBeInTheDocument(); expect(screen.getByText(t(Keys.containerCopy.selectContainers.createContainerButtonLabel))).toBeInTheDocument();
}); });
}); });
@@ -1,6 +1,6 @@
import { ActionButton, Dropdown, Stack } from "@fluentui/react"; import { ActionButton, Dropdown, Stack } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { DatabaseContainerSectionProps } from "../../../../Types/CopyJobTypes"; import { DatabaseContainerSectionProps } from "../../../../Types/CopyJobTypes";
import FieldRow from "../../Components/FieldRow"; import FieldRow from "../../Components/FieldRow";
@@ -19,10 +19,10 @@ export const DatabaseContainerSection = ({
}: DatabaseContainerSectionProps) => ( }: DatabaseContainerSectionProps) => (
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection"> <Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
<label className="subHeading">{heading}</label> <label className="subHeading">{heading}</label>
<FieldRow label={ContainerCopyMessages.databaseDropdownLabel}> <FieldRow label={t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.databaseDropdownPlaceholder} placeholder={t(Keys.containerCopy.selectContainers.databaseDropdownPlaceholder)}
ariaLabel={ContainerCopyMessages.databaseDropdownLabel} ariaLabel={t(Keys.containerCopy.selectContainers.databaseDropdownLabel)}
options={databaseOptions} options={databaseOptions}
required required
disabled={!!databaseDisabled} disabled={!!databaseDisabled}
@@ -31,11 +31,11 @@ export const DatabaseContainerSection = ({
data-test={`${sectionType}-databaseDropdown`} data-test={`${sectionType}-databaseDropdown`}
/> />
</FieldRow> </FieldRow>
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}> <FieldRow label={t(Keys.containerCopy.selectContainers.containerDropdownLabel)}>
<Stack> <Stack>
<Dropdown <Dropdown
placeholder={ContainerCopyMessages.containerDropdownPlaceholder} placeholder={t(Keys.containerCopy.selectContainers.containerDropdownPlaceholder)}
ariaLabel={ContainerCopyMessages.containerDropdownLabel} ariaLabel={t(Keys.containerCopy.selectContainers.containerDropdownLabel)}
options={containerOptions} options={containerOptions}
required required
disabled={!!containerDisabled} disabled={!!containerDisabled}
@@ -49,7 +49,7 @@ export const DatabaseContainerSection = ({
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
onClick={() => handleOnDemandCreateContainer()} onClick={() => handleOnDemandCreateContainer()}
> >
{ContainerCopyMessages.createContainerButtonLabel} {t(Keys.containerCopy.selectContainers.createContainerButtonLabel)}
</ActionButton> </ActionButton>
)} )}
</Stack> </Stack>
@@ -69,15 +69,15 @@ describe("useSourceAndTargetData", () => {
const mockCopyJobState: CopyJobContextState = { const mockCopyJobState: CopyJobContextState = {
jobName: "test-job", jobName: "test-job",
migrationType: CopyJobMigrationType.Offline, migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: false, sourceReadWriteAccessFromTarget: 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",
}, },
], ],
@@ -20,28 +20,6 @@ jest.mock("../../../Controls/Dialog", () => ({
}, },
})); }));
jest.mock("../../ContainerCopyMessages", () => ({
__esModule: true,
default: {
MonitorJobs: {
Columns: {
actions: "Actions",
},
Actions: {
pause: "Pause",
resume: "Resume",
cancel: "Cancel",
complete: "Complete",
},
dialog: {
heading: "Confirm Action",
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
},
},
},
}));
describe("CopyJobActionMenu", () => { describe("CopyJobActionMenu", () => {
const createMockJob = (overrides: Partial<CopyJobType> = {}): CopyJobType => const createMockJob = (overrides: Partial<CopyJobType> = {}): CopyJobType =>
({ ({
@@ -301,8 +279,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(cancelButton); fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", "",
null, "",
"Confirm", "Confirm",
expect.any(Function), expect.any(Function),
"Cancel", "Cancel",
@@ -358,8 +336,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(completeButton); fireEvent.click(completeButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", "",
null, "",
"Confirm", "Confirm",
expect.any(Function), expect.any(Function),
"Cancel", "Cancel",
@@ -402,8 +380,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(cancelButton); fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", "",
null, "",
"Confirm", "Confirm",
expect.any(Function), expect.any(Function),
"Cancel", "Cancel",
@@ -433,8 +411,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(completeButton); fireEvent.click(completeButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", "",
null, "",
"Confirm", "Confirm",
expect.any(Function), expect.any(Function),
"Cancel", "Cancel",
@@ -849,8 +827,8 @@ describe("CopyJobActionMenu", () => {
fireEvent.click(cancelButton); fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith( expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", // title "", // title
null, // subText "", // subText
"Confirm", // confirmLabel "Confirm", // confirmLabel
expect.any(Function), // onOk expect.any(Function), // onOk
"Cancel", // cancelLabel "Cancel", // cancelLabel
@@ -1,7 +1,7 @@
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react"; import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { useDialog } from "../../../Controls/Dialog"; import { useDialog } from "../../../Controls/Dialog";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
@@ -49,11 +49,11 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
useDialog useDialog
.getState() .getState()
.showOkCancelModalDialog( .showOkCancelModalDialog(
ContainerCopyMessages.MonitorJobs.dialog.heading, "",
null, "",
ContainerCopyMessages.MonitorJobs.dialog.confirmButtonText, t(Keys.common.confirm),
() => handleClick(job, action, setUpdatingJobAction), () => handleClick(job, action, setUpdatingJobAction),
ContainerCopyMessages.MonitorJobs.dialog.cancelButtonText, t(Keys.common.cancel),
null, null,
action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null, action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null,
); );
@@ -65,21 +65,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
const baseItems = [ const baseItems = [
{ {
key: CopyJobActions.pause, key: CopyJobActions.pause,
text: ContainerCopyMessages.MonitorJobs.Actions.pause, text: t(Keys.containerCopy.monitorJobs.actions.pause),
iconProps: { iconName: "Pause" }, iconProps: { iconName: "Pause" },
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction), onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
disabled: isThisJobUpdating, disabled: isThisJobUpdating,
}, },
{ {
key: CopyJobActions.cancel, key: CopyJobActions.cancel,
text: ContainerCopyMessages.MonitorJobs.Actions.cancel, text: t(Keys.common.cancel),
iconProps: { iconName: "Cancel" }, iconProps: { iconName: "Cancel" },
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel), onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
disabled: isThisJobUpdating, disabled: isThisJobUpdating,
}, },
{ {
key: CopyJobActions.resume, key: CopyJobActions.resume,
text: ContainerCopyMessages.MonitorJobs.Actions.resume, text: t(Keys.containerCopy.monitorJobs.actions.resume),
iconProps: { iconName: "Play" }, iconProps: { iconName: "Play" },
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction), onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
disabled: isThisJobUpdating, disabled: isThisJobUpdating,
@@ -101,7 +101,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
if ((job.Mode ?? "").toLowerCase() === CopyJobMigrationType.Online) { if ((job.Mode ?? "").toLowerCase() === CopyJobMigrationType.Online) {
filteredItems.push({ filteredItems.push({
key: CopyJobActions.complete, key: CopyJobActions.complete,
text: ContainerCopyMessages.MonitorJobs.Actions.complete, text: t(Keys.containerCopy.monitorJobs.actions.complete),
iconProps: { iconName: "CheckMark" }, iconProps: { iconName: "CheckMark" },
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete), onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
disabled: isThisJobUpdating, disabled: isThisJobUpdating,
@@ -124,8 +124,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }} iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }} menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
menuIconProps={{ iconName: "", className: "hidden" }} menuIconProps={{ iconName: "", className: "hidden" }}
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions} ariaLabel={t(Keys.containerCopy.monitorJobs.columns.actions)}
title={ContainerCopyMessages.MonitorJobs.Columns.actions} title={t(Keys.containerCopy.monitorJobs.columns.actions)}
/> />
); );
}; };
@@ -1,8 +1,8 @@
import { IColumn } from "@fluentui/react"; import { IColumn } from "@fluentui/react";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
import { getColumns } from "./CopyJobColumns"; import { getColumns } from "./CopyJobColumns";
@@ -79,14 +79,14 @@ describe("CopyJobColumns", () => {
expect(actualKeys).toEqual(expectedKeys); expect(actualKeys).toEqual(expectedKeys);
}); });
it("should have correct column names from ContainerCopyMessages", () => { it("should have correct column names", () => {
const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false); const columns = getColumns(mockHandleSort, mockHandleActionClick, undefined, false);
expect(columns[0].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime); expect(columns[0].name).toBe(t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime));
expect(columns[1].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.name); expect(columns[1].name).toBe(t(Keys.containerCopy.monitorJobs.columns.name));
expect(columns[2].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.mode); expect(columns[2].name).toBe(t(Keys.containerCopy.monitorJobs.columns.mode));
expect(columns[3].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.completionPercentage); expect(columns[3].name).toBe(t(Keys.containerCopy.monitorJobs.columns.completionPercentage));
expect(columns[4].name).toBe(ContainerCopyMessages.MonitorJobs.Columns.status); expect(columns[4].name).toBe(t(Keys.containerCopy.monitorJobs.columns.status));
expect(columns[5].name).toBe(""); expect(columns[5].name).toBe("");
}); });
@@ -1,6 +1,6 @@
import { IColumn } from "@fluentui/react"; import { IColumn } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
import CopyJobActionMenu from "./CopyJobActionMenu"; import CopyJobActionMenu from "./CopyJobActionMenu";
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon"; import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
@@ -13,7 +13,7 @@ export const getColumns = (
): IColumn[] => [ ): IColumn[] => [
{ {
key: "LastUpdatedTime", key: "LastUpdatedTime",
name: ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime, name: t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime),
fieldName: "LastUpdatedTime", fieldName: "LastUpdatedTime",
minWidth: 140, minWidth: 140,
maxWidth: 300, maxWidth: 300,
@@ -24,7 +24,7 @@ export const getColumns = (
}, },
{ {
key: "Name", key: "Name",
name: ContainerCopyMessages.MonitorJobs.Columns.name, name: t(Keys.containerCopy.monitorJobs.columns.name),
fieldName: "Name", fieldName: "Name",
minWidth: 140, minWidth: 140,
maxWidth: 300, maxWidth: 300,
@@ -36,7 +36,7 @@ export const getColumns = (
}, },
{ {
key: "Mode", key: "Mode",
name: ContainerCopyMessages.MonitorJobs.Columns.mode, name: t(Keys.containerCopy.monitorJobs.columns.mode),
fieldName: "Mode", fieldName: "Mode",
minWidth: 90, minWidth: 90,
maxWidth: 200, maxWidth: 200,
@@ -47,7 +47,7 @@ export const getColumns = (
}, },
{ {
key: "CompletionPercentage", key: "CompletionPercentage",
name: ContainerCopyMessages.MonitorJobs.Columns.completionPercentage, name: t(Keys.containerCopy.monitorJobs.columns.completionPercentage),
fieldName: "CompletionPercentage", fieldName: "CompletionPercentage",
minWidth: 110, minWidth: 110,
maxWidth: 200, maxWidth: 200,
@@ -59,7 +59,7 @@ export const getColumns = (
}, },
{ {
key: "CopyJobStatus", key: "CopyJobStatus",
name: ContainerCopyMessages.MonitorJobs.Columns.status, name: t(Keys.containerCopy.monitorJobs.columns.status),
fieldName: "Status", fieldName: "Status",
minWidth: 130, minWidth: 130,
maxWidth: 200, maxWidth: 200,
@@ -13,22 +13,6 @@ jest.mock("./CopyJobStatusWithIcon", () => {
return MockCopyJobStatusWithIcon; return MockCopyJobStatusWithIcon;
}); });
jest.mock("../../ContainerCopyMessages", () => ({
errorTitle: "Error Details",
sourceDatabaseLabel: "Source Database",
sourceContainerLabel: "Source Container",
targetDatabaseLabel: "Destination Database",
targetContainerLabel: "Destination Container",
sourceAccountLabel: "Source Account",
MonitorJobs: {
Columns: {
lastUpdatedTime: "Date & time",
status: "Status",
mode: "Mode",
},
},
}));
describe("CopyJobDetails", () => { describe("CopyJobDetails", () => {
const mockBasicJob: CopyJobType = { const mockBasicJob: CopyJobType = {
ID: "test-job-1", ID: "test-job-1",
@@ -102,8 +86,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 +247,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 +306,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");
}); });
}); });
@@ -1,7 +1,7 @@
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react"; import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import React, { memo } from "react"; import React, { memo } from "react";
import { useThemeStore } from "../../../../hooks/useTheme"; import { useThemeStore } from "../../../../hooks/useTheme";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType } from "../../Types/CopyJobTypes"; import { CopyJobType } from "../../Types/CopyJobTypes";
import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon"; import CopyJobStatusWithIcon from "./CopyJobStatusWithIcon";
@@ -31,31 +31,31 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
return [ return [
{ {
key: "sourcedbcol", key: "sourcedbcol",
name: ContainerCopyMessages.sourceDatabaseLabel, name: t(Keys.containerCopy.preview.sourceDatabaseLabel),
fieldName: "sourceDatabaseName", fieldName: "sourceDatabaseName",
...commonProps, ...commonProps,
}, },
{ {
key: "sourcecol", key: "sourcecol",
name: ContainerCopyMessages.sourceContainerLabel, name: t(Keys.containerCopy.preview.sourceContainerLabel),
fieldName: "sourceContainerName", fieldName: "sourceContainerName",
...commonProps, ...commonProps,
}, },
{ {
key: "targetdbcol", key: "targetdbcol",
name: ContainerCopyMessages.targetDatabaseLabel, name: t(Keys.containerCopy.preview.targetDatabaseLabel),
fieldName: "targetDatabaseName", fieldName: "targetDatabaseName",
...commonProps, ...commonProps,
}, },
{ {
key: "targetcol", key: "targetcol",
name: ContainerCopyMessages.targetContainerLabel, name: t(Keys.containerCopy.preview.targetContainerLabel),
fieldName: "targetContainerName", fieldName: "targetContainerName",
...commonProps, ...commonProps,
}, },
{ {
key: "statuscol", key: "statuscol",
name: ContainerCopyMessages.MonitorJobs.Columns.status, name: t(Keys.containerCopy.monitorJobs.columns.status),
fieldName: "jobStatus", fieldName: "jobStatus",
onRender: ({ jobStatus }: { jobStatus: CopyJobStatusType }) => <CopyJobStatusWithIcon status={jobStatus} />, onRender: ({ jobStatus }: { jobStatus: CopyJobStatusType }) => <CopyJobStatusWithIcon status={jobStatus} />,
...commonProps, ...commonProps,
@@ -92,7 +92,7 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
{job.Error ? ( {job.Error ? (
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}> <Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
<Text className="bold themeText" style={sectionCss.headingText}> <Text className="bold themeText" style={sectionCss.headingText}>
{ContainerCopyMessages.errorTitle} {t(Keys.containerCopy.jobDetails.errorTitle)}
</Text> </Text>
<Text as="pre" style={errorMessageStyle}> <Text as="pre" style={errorMessageStyle}>
{job.Error.message} {job.Error.message}
@@ -102,15 +102,15 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
<Stack.Item data-testid="selectedcollection-stack"> <Stack.Item data-testid="selectedcollection-stack">
<Stack tokens={{ childrenGap: 15 }}> <Stack tokens={{ childrenGap: 15 }}>
<Stack.Item style={sectionCss.verticalAlign}> <Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text> <Text className="bold themeText">{t(Keys.containerCopy.monitorJobs.columns.lastUpdatedTime)}</Text>
<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">{t(Keys.containerCopy.preview.accountLabel)}</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">{t(Keys.containerCopy.monitorJobs.columns.mode)}</Text>
<Text className="themeText">{job.Mode}</Text> <Text className="themeText">{job.Mode}</Text>
</Stack.Item> </Stack.Item>
</Stack> </Stack>
@@ -1,7 +1,7 @@
import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react"; import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import React from "react"; import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
const iconClass = mergeStyles({ const iconClass = mergeStyles({
@@ -30,12 +30,25 @@ const statusIconColors: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Paused]: "var(--colorBrandForeground1)", [CopyJobStatusType.Paused]: "var(--colorBrandForeground1)",
}; };
const statusKeyMap: Record<CopyJobStatusType, string> = {
[CopyJobStatusType.Pending]: Keys.containerCopy.monitorJobs.status.pending,
[CopyJobStatusType.InProgress]: Keys.containerCopy.monitorJobs.status.inProgress,
[CopyJobStatusType.Running]: Keys.containerCopy.monitorJobs.status.running,
[CopyJobStatusType.Partitioning]: Keys.containerCopy.monitorJobs.status.partitioning,
[CopyJobStatusType.Paused]: Keys.containerCopy.monitorJobs.status.paused,
[CopyJobStatusType.Completed]: Keys.containerCopy.monitorJobs.status.completed,
[CopyJobStatusType.Failed]: Keys.containerCopy.monitorJobs.status.failed,
[CopyJobStatusType.Faulted]: Keys.containerCopy.monitorJobs.status.faulted,
[CopyJobStatusType.Skipped]: Keys.containerCopy.monitorJobs.status.skipped,
[CopyJobStatusType.Cancelled]: Keys.containerCopy.monitorJobs.status.cancelled,
};
export interface CopyJobStatusWithIconProps { export interface CopyJobStatusWithIconProps {
status: CopyJobStatusType; status: CopyJobStatusType;
} }
const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo(({ status }) => { const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo(({ status }) => {
const statusText = ContainerCopyMessages.MonitorJobs.Status[status] || "Unknown"; const statusText = statusKeyMap[status] ? t(statusKeyMap[status] as Parameters<typeof t>[0]) : "Unknown";
const isSpinnerStatus = [ const isSpinnerStatus = [
CopyJobStatusType.Running, CopyJobStatusType.Running,
@@ -3,9 +3,9 @@ jest.mock("../../Actions/CopyJobActions");
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import * as Actions from "../../Actions/CopyJobActions"; import * as Actions from "../../Actions/CopyJobActions";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import CopyJobsNotFound from "./CopyJobs.NotFound"; import CopyJobsNotFound from "./CopyJobs.NotFound";
describe("CopyJobsNotFound", () => { describe("CopyJobsNotFound", () => {
@@ -22,10 +22,10 @@ describe("CopyJobsNotFound", () => {
const image = container.querySelector(".notFoundContainer .ms-Image"); const image = container.querySelector(".notFoundContainer .ms-Image");
expect(image).toBeInTheDocument(); expect(image).toBeInTheDocument();
expect(image).toHaveAttribute("style", "width: 100px; height: 100px;"); expect(image).toHaveAttribute("style", "width: 100px; height: 100px;");
expect(getByText(ContainerCopyMessages.noCopyJobsTitle)).toBeInTheDocument(); expect(getByText(t(Keys.containerCopy.noCopyJobs.title))).toBeInTheDocument();
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.createCopyJobButtonText, name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
expect(button).toHaveClass("createCopyJobButton"); expect(button).toHaveClass("createCopyJobButton");
@@ -45,7 +45,7 @@ describe("CopyJobsNotFound", () => {
render(<CopyJobsNotFound explorer={mockExplorer} />); render(<CopyJobsNotFound explorer={mockExplorer} />);
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.createCopyJobButtonText, name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
}); });
fireEvent.click(button); fireEvent.click(button);
@@ -58,11 +58,11 @@ describe("CopyJobsNotFound", () => {
render(<CopyJobsNotFound explorer={mockExplorer} />); render(<CopyJobsNotFound explorer={mockExplorer} />);
const button = screen.getByRole("button", { const button = screen.getByRole("button", {
name: ContainerCopyMessages.createCopyJobButtonText, name: t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
expect(button.textContent).toBe(ContainerCopyMessages.createCopyJobButtonText); expect(button.textContent).toBe(t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText));
}); });
it("should use memo to prevent unnecessary re-renders", () => { it("should use memo to prevent unnecessary re-renders", () => {
@@ -1,9 +1,9 @@
import { ActionButton, Image } from "@fluentui/react"; import { ActionButton, Image } from "@fluentui/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg"; import CopyJobIcon from "../../../../../images/ContainerCopy/copy-jobs.svg";
import * as Actions from "../../Actions/CopyJobActions"; import * as Actions from "../../Actions/CopyJobActions";
import ContainerCopyMessages from "../../ContainerCopyMessages";
interface CopyJobsNotFoundProps { interface CopyJobsNotFoundProps {
explorer: Explorer; explorer: Explorer;
@@ -12,14 +12,14 @@ interface CopyJobsNotFoundProps {
const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => { const CopyJobsNotFound: React.FC<CopyJobsNotFoundProps> = ({ explorer }) => {
return ( return (
<div className="notFoundContainer flexContainer centerContent"> <div className="notFoundContainer flexContainer centerContent">
<Image src={CopyJobIcon} alt={ContainerCopyMessages.noCopyJobsTitle} width={100} height={100} /> <Image src={CopyJobIcon} alt={t(Keys.containerCopy.noCopyJobs.title)} width={100} height={100} />
<h4 className="noCopyJobsMessage">{ContainerCopyMessages.noCopyJobsTitle}</h4> <h4 className="noCopyJobsMessage">{t(Keys.containerCopy.noCopyJobs.title)}</h4>
<ActionButton <ActionButton
allowDisabledFocus allowDisabledFocus
className="createCopyJobButton" className="createCopyJobButton"
onClick={() => Actions.openCreateCopyJobPanel(explorer)} onClick={() => Actions.openCreateCopyJobPanel(explorer)}
> >
{ContainerCopyMessages.createCopyJobButtonText} {t(Keys.containerCopy.noCopyJobs.createCopyJobButtonText)}
</ActionButton> </ActionButton>
</div> </div>
); );
@@ -55,15 +55,15 @@ export interface DatabaseContainerSectionProps {
export interface CopyJobContextState { export interface CopyJobContextState {
jobName: string; jobName: string;
migrationType: CopyJobMigrationType; migrationType: CopyJobMigrationType;
sourceReadAccessFromTarget?: boolean; sourceReadWriteAccessFromTarget?: 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;
@@ -10,7 +10,7 @@ import {
Stack, Stack,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels"; import { AccountOverride, FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import * as React from "react"; import * as React from "react";
import { isFullTextSearchPreviewFeaturesEnabled } from "Utils/CapabilityUtils"; import { isFullTextSearchPreviewFeaturesEnabled } from "Utils/CapabilityUtils";
@@ -25,6 +25,7 @@ export interface FullTextPoliciesComponentProps {
discardChanges?: boolean; discardChanges?: boolean;
onChangesDiscarded?: () => void; onChangesDiscarded?: () => void;
englishOnly?: boolean; englishOnly?: boolean;
targetAccountOverride?: AccountOverride;
} }
export interface FullTextPolicyData { export interface FullTextPolicyData {
@@ -206,6 +207,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
discardChanges, discardChanges,
onChangesDiscarded, onChangesDiscarded,
englishOnly, englishOnly,
targetAccountOverride,
}): JSX.Element => { }): JSX.Element => {
const getFullTextPathError = (path: string, index?: number): string => { const getFullTextPathError = (path: string, index?: number): string => {
let error = ""; let error = "";
@@ -236,7 +238,9 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy)); const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy));
const [defaultLanguage, setDefaultLanguage] = React.useState<string>( const [defaultLanguage, setDefaultLanguage] = React.useState<string>(
fullTextPolicy ? fullTextPolicy.defaultLanguage : (getFullTextLanguageOptions()[0].key as never), fullTextPolicy
? fullTextPolicy.defaultLanguage
: (getFullTextLanguageOptions(englishOnly, targetAccountOverride)[0].key as never),
); );
React.useEffect(() => { React.useEffect(() => {
@@ -307,7 +311,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
<Dropdown <Dropdown
required={true} required={true}
styles={dropdownStyles} styles={dropdownStyles}
options={getFullTextLanguageOptions(englishOnly)} options={getFullTextLanguageOptions(englishOnly, targetAccountOverride)}
selectedKey={defaultLanguage} selectedKey={defaultLanguage}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) => onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
setDefaultLanguage(option.key as never) setDefaultLanguage(option.key as never)
@@ -352,7 +356,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
<Dropdown <Dropdown
required={true} required={true}
styles={dropdownStyles} styles={dropdownStyles}
options={getFullTextLanguageOptions(englishOnly)} options={getFullTextLanguageOptions(englishOnly, targetAccountOverride)}
selectedKey={fullTextPolicy.language} selectedKey={fullTextPolicy.language}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) => onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onFullTextPathPolicyChange(index, option) onFullTextPathPolicyChange(index, option)
@@ -395,8 +399,12 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
); );
}; };
export const getFullTextLanguageOptions = (englishOnly?: boolean): IDropdownOption[] => { export const getFullTextLanguageOptions = (
const multiLanguageSupportEnabled: boolean = isFullTextSearchPreviewFeaturesEnabled() && !englishOnly; englishOnly?: boolean,
targetAccountOverride?: AccountOverride,
): IDropdownOption[] => {
const multiLanguageSupportEnabled: boolean =
isFullTextSearchPreviewFeaturesEnabled(targetAccountOverride) && !englishOnly;
const fullTextLanguageOptions: IDropdownOption[] = [ const fullTextLanguageOptions: IDropdownOption[] = [
{ {
key: "en-US", key: "en-US",
@@ -1,23 +1,85 @@
import { shallow } from "enzyme"; import { render, screen, waitFor } from "@testing-library/react";
import { DatabaseAccount } from "Contracts/DataModels";
import { import {
PartitionKeyComponent, PartitionKeyComponent,
PartitionKeyComponentProps, PartitionKeyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent"; } from "Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
import Explorer from "Explorer/Explorer"; import { useDataTransferJobs } from "hooks/useDataTransferJobs";
import React from "react"; import React from "react";
import { updateUserContext } from "UserContext";
import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types";
import Explorer from "../../../Explorer";
jest.mock("Common/dataAccess/dataTransfers", () => ({
cancelDataTransferJob: jest.fn().mockResolvedValue(undefined),
pauseDataTransferJob: jest.fn().mockResolvedValue(undefined),
resumeDataTransferJob: jest.fn().mockResolvedValue(undefined),
completeDataTransferJob: jest.fn().mockResolvedValue(undefined),
pollDataTransferJob: jest.fn().mockResolvedValue(undefined),
}));
jest.mock("hooks/useDataTransferJobs", () => ({
useDataTransferJobs: jest.fn(() => ({ dataTransferJobs: [] })),
refreshDataTransferJobs: jest.fn().mockResolvedValue(undefined),
}));
jest.mock("hooks/useSidePanel", () => ({
useSidePanel: {
getState: () => ({
openSidePanel: jest.fn(),
}),
},
}));
jest.mock("ConfigContext", () => ({
configContext: { platform: "Portal" },
Platform: { Emulator: "Emulator", Portal: "Portal" },
}));
jest.mock("Explorer/Explorer", () => {
return jest.fn().mockImplementation(() => ({
refreshAllDatabases: jest.fn(),
refreshExplorer: jest.fn(),
}));
});
const mockOfflineJob = {
properties: {
jobName: "Portal_test_123",
source: { component: "CosmosDBSql" as const, databaseName: "testDb", containerName: "testCol" },
destination: { component: "CosmosDBSql" as const, databaseName: "testDb", containerName: "newCol" },
status: "InProgress",
processedCount: 50,
totalCount: 100,
mode: "Offline" as const,
},
} as DataTransferJobGetResults;
const mockOnlineJob = {
properties: {
jobName: "Portal_test_456",
source: { component: "CosmosDBSql" as const, databaseName: "testDb", containerName: "testCol" },
destination: { component: "CosmosDBSql" as const, databaseName: "testDb", containerName: "newCol" },
status: "InProgress",
processedCount: 50,
totalCount: 100,
mode: "Online" as const,
},
} as DataTransferJobGetResults;
describe("PartitionKeyComponent", () => { describe("PartitionKeyComponent", () => {
// Create a test setup function to get fresh instances for each test
const setupTest = () => { const setupTest = () => {
// Create an instance of the mocked Explorer
const explorer = new Explorer(); const explorer = new Explorer();
// Create minimal mock objects for database and collection
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockDatabase = {} as any as import("../../../../Contracts/ViewModels").Database; const mockDatabase = {} as any as import("../../../../Contracts/ViewModels").Database;
// eslint-disable-next-line @typescript-eslint/no-explicit-any const mockCollection = {
const mockCollection = {} as any as import("../../../../Contracts/ViewModels").Collection; id: jest.fn().mockReturnValue("testCol"),
databaseId: "testDb",
partitionKey: { kind: "Hash", paths: ["/id"], version: 2 },
partitionKeyProperties: ["id"],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any as import("../../../../Contracts/ViewModels").Collection;
// Create props with the mocked Explorer instance
const props: PartitionKeyComponentProps = { const props: PartitionKeyComponentProps = {
database: mockDatabase, database: mockDatabase,
collection: mockCollection, collection: mockCollection,
@@ -27,15 +89,53 @@ describe("PartitionKeyComponent", () => {
return { explorer, props }; return { explorer, props };
}; };
it("renders default component and matches snapshot", () => { beforeEach(() => {
const { props } = setupTest(); jest.clearAllMocks();
const wrapper = shallow(<PartitionKeyComponent {...props} />); updateUserContext({
expect(wrapper).toMatchSnapshot(); databaseAccount: {
name: "testAccount",
id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/testAccount",
properties: {
documentEndpoint: "https://test.documents.azure.com",
},
} as unknown as DatabaseAccount,
subscriptionId: "sub1",
resourceGroup: "rg1",
});
}); });
it("renders read-only component and matches snapshot", () => { it("renders partition key value", () => {
const { props } = setupTest(); const { props } = setupTest();
const wrapper = shallow(<PartitionKeyComponent {...props} isReadOnly={true} />); render(<PartitionKeyComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(screen.getByText("/id")).toBeTruthy();
});
it("renders read-only component without change button", () => {
const { props } = setupTest();
const { container } = render(<PartitionKeyComponent {...props} isReadOnly={true} />);
expect(container.querySelector("[data-test='change-partition-key-button']")).toBeNull();
});
it("shows cancel button for offline job in progress", () => {
(useDataTransferJobs as unknown as jest.Mock).mockReturnValue({
dataTransferJobs: [mockOfflineJob],
});
const { props } = setupTest();
const { container } = render(<PartitionKeyComponent {...props} />);
// For offline jobs, the online action menu should not be present
expect(container.querySelector("[data-test='online-job-action-menu']")).toBeNull();
});
it("shows ellipsis action menu for online job in progress", async () => {
(useDataTransferJobs as unknown as jest.Mock).mockReturnValue({
dataTransferJobs: [mockOnlineJob],
});
const { props } = setupTest();
const { container } = render(<PartitionKeyComponent {...props} />);
await waitFor(() => {
expect(container.querySelector("[data-test='online-job-action-menu']")).toBeTruthy();
});
}); });
}); });
@@ -1,7 +1,10 @@
import { import {
DefaultButton, DefaultButton,
DirectionalHint,
FontWeights, FontWeights,
IContextualMenuProps,
IMessageBarStyles, IMessageBarStyles,
IconButton,
Link, Link,
MessageBar, MessageBar,
MessageBarType, MessageBarType,
@@ -14,8 +17,16 @@ import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers"; import {
cancelDataTransferJob,
completeDataTransferJob,
pauseDataTransferJob,
pollDataTransferJob,
resumeDataTransferJob,
} from "Common/dataAccess/dataTransfers";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { CopyJobActions, CopyJobMigrationType } from "Explorer/ContainerCopy/Enums/CopyJobEnums";
import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane"; import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
import { Keys, t } from "Localization"; import { Keys, t } from "Localization";
@@ -94,11 +105,11 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
const textSubHeadingStyle1 = { const textSubHeadingStyle1 = {
root: { color: "var(--colorNeutralForeground1)" }, root: { color: "var(--colorNeutralForeground1)" },
}; };
const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => { const startPollingforUpdate = async (currentJob: DataTransferJobGetResults) => {
if (isCurrentJobInProgress(currentJob)) { if (isCurrentJobInProgress(currentJob)) {
const jobName = currentJob?.properties?.jobName; const jobName = currentJob?.properties?.jobName;
try { try {
pollDataTransferJob( await pollDataTransferJob(
jobName, jobName,
userContext.subscriptionId, userContext.subscriptionId,
userContext.resourceGroup, userContext.resourceGroup,
@@ -119,6 +130,124 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
); );
}; };
const pauseRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
await pauseDataTransferJob(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
currentJob?.properties?.jobName,
);
};
const resumePausedDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
await resumeDataTransferJob(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
currentJob?.properties?.jobName,
);
startPollingforUpdate(currentJob);
};
const completeOnlineDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
await completeDataTransferJob(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
currentJob?.properties?.jobName,
);
};
const isOnlineJob = (currentJob: DataTransferJobGetResults): boolean => {
const mode = (currentJob?.properties?.mode ?? "").toLowerCase();
return mode === CopyJobMigrationType.Online;
};
const showActionConfirmationDialog = (
currentJob: DataTransferJobGetResults,
action: CopyJobActions,
onConfirm: () => void,
): void => {
const jobName = currentJob?.properties?.jobName;
const dialogBody =
action === CopyJobActions.cancel ? (
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
{t(Keys.controls.settings.partitionKeyEditor.confirmCancel1)}
<br />
<b>{jobName}</b>
</Stack.Item>
<Stack.Item>{t(Keys.controls.settings.partitionKeyEditor.confirmCancel2)}</Stack.Item>
</Stack>
) : action === CopyJobActions.complete ? (
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
{t(Keys.controls.settings.partitionKeyEditor.confirmComplete1)}
<br />
<b>{jobName}</b>
</Stack.Item>
<Stack.Item>{t(Keys.controls.settings.partitionKeyEditor.confrimComplete2)}</Stack.Item>
</Stack>
) : null;
useDialog
.getState()
.showOkCancelModalDialog("", null, t(Keys.common.confirm), onConfirm, t(Keys.common.cancel), null, dialogBody);
};
const getOnlineJobMenuProps = (currentJob: DataTransferJobGetResults): IContextualMenuProps => {
const jobStatus = currentJob?.properties?.status;
const isPaused = jobStatus === "Paused";
const items: IContextualMenuProps["items"] = [];
if (!isPaused) {
items.push({
key: CopyJobActions.pause,
text: t(Keys.containerCopy.monitorJobs.actions.pause),
iconProps: { iconName: "Pause" },
onClick: () => {
pauseRunningDataTransferJob(currentJob);
},
});
}
if (isPaused) {
items.push({
key: CopyJobActions.resume,
text: t(Keys.containerCopy.monitorJobs.actions.resume),
iconProps: { iconName: "Play" },
onClick: () => {
resumePausedDataTransferJob(currentJob);
},
});
}
items.push({
key: CopyJobActions.cancel,
text: t(Keys.common.cancel),
iconProps: { iconName: "Cancel" },
onClick: () =>
showActionConfirmationDialog(currentJob, CopyJobActions.cancel, () => cancelRunningDataTransferJob(currentJob)),
});
items.push({
key: CopyJobActions.complete,
text: t(Keys.containerCopy.monitorJobs.actions.complete),
iconProps: { iconName: "CheckMark" },
onClick: () =>
showActionConfirmationDialog(currentJob, CopyJobActions.complete, () =>
completeOnlineDataTransferJob(currentJob),
),
});
return {
items,
directionalHint: DirectionalHint.leftTopEdge,
directionalHintFixed: false,
};
};
const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => { const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => {
const jobStatus = currentJob?.properties?.status; const jobStatus = currentJob?.properties?.status;
return ( return (
@@ -269,12 +398,26 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
}, },
}} }}
></ProgressIndicator> ></ProgressIndicator>
{isCurrentJobInProgress(portalDataTransferJob) && ( {isCurrentJobInProgress(portalDataTransferJob) &&
<DefaultButton (isOnlineJob(portalDataTransferJob) ? (
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)} <IconButton
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} data-test="online-job-action-menu"
/> role="button"
)} iconProps={{
iconName: "More",
styles: { root: { fontSize: "20px", fontWeight: "bold" } },
}}
menuProps={getOnlineJobMenuProps(portalDataTransferJob)}
menuIconProps={{ iconName: "", className: "hidden" }}
ariaLabel={t(Keys.containerCopy.monitorJobs.columns.actions)}
title={t(Keys.containerCopy.monitorJobs.columns.actions)}
/>
) : (
<DefaultButton
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)}
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)}
/>
))}
</Stack> </Stack>
</Stack> </Stack>
)} )}
@@ -1,271 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PartitionKeyComponent renders default component and matches snapshot 1`] = `
<Stack
styles={
{
"root": {
"maxWidth": 600,
},
}
}
tokens={
{
"childrenGap": 20,
}
}
>
<Stack
tokens={
{
"childrenGap": 10,
}
}
>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
"fontSize": 16,
"fontWeight": 600,
},
}
}
>
Change partition key
</Text>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 20,
}
}
>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
"fontWeight": 600,
},
}
}
>
Current partition key
</Text>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
"fontWeight": 600,
},
}
}
>
Partitioning
</Text>
</Stack>
<Stack
data-test="partition-key-values"
tokens={
{
"childrenGap": 5,
}
}
>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
},
}
}
/>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
},
}
}
>
Non-hierarchical
</Text>
</Stack>
</Stack>
</Stack>
<StyledMessageBar
data-test="partition-key-warning"
messageBarIconProps={
{
"className": "messageBarWarningIcon",
"iconName": "WarningSolid",
}
}
messageBarType={5}
styles={
{
"root": {
"selectors": {
"&.ms-MessageBar--warning": {
"backgroundColor": "var(--colorStatusWarningBackground1)",
"border": "1px solid var(--colorStatusWarningBorder1)",
},
".ms-MessageBar-icon": {
"color": "var(--colorNeutralForeground1)",
},
".ms-MessageBar-text": {
"color": "var(--colorNeutralForeground1)",
},
},
},
}
}
>
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the source container for the entire duration of the partition key change process.
<StyledLinkBase
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
style={
{
"color": "var(--colorBrandForeground1)",
}
}
target="_blank"
underline={true}
>
Learn more
</StyledLinkBase>
</StyledMessageBar>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
},
}
}
>
To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container.
</Text>
<CustomizedPrimaryButton
data-test="change-partition-key-button"
onClick={[Function]}
styles={
{
"root": {
"width": "fit-content",
},
}
}
text="Change"
/>
</Stack>
`;
exports[`PartitionKeyComponent renders read-only component and matches snapshot 1`] = `
<Stack
styles={
{
"root": {
"maxWidth": 600,
},
}
}
tokens={
{
"childrenGap": 20,
}
}
>
<Stack
tokens={
{
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 20,
}
}
>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
"fontWeight": 600,
},
}
}
>
Current partition key
</Text>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
"fontWeight": 600,
},
}
}
>
Partitioning
</Text>
</Stack>
<Stack
data-test="partition-key-values"
tokens={
{
"childrenGap": 5,
}
}
>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
},
}
}
/>
<Text
styles={
{
"root": {
"color": "var(--colorNeutralForeground1)",
},
}
}
>
Non-hierarchical
</Text>
</Stack>
</Stack>
</Stack>
</Stack>
`;
@@ -291,7 +291,7 @@ describe("SettingsUtils", () => {
it("handles partition key tab title based on fabric native", () => { it("handles partition key tab title based on fabric native", () => {
// Assuming initially not fabric native // Assuming initially not fabric native
expect(getTabTitle(SettingsV2TabTypes.PartitionKeyTab)).toBe("Partition Keys (preview)"); expect(getTabTitle(SettingsV2TabTypes.PartitionKeyTab)).toBe("Partition Keys");
}); });
it("throws error for unknown tab type", () => { it("throws error for unknown tab type", () => {
@@ -2,7 +2,6 @@ import { Keys, t } from "Localization";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils"; import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils";
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
@@ -185,9 +184,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
case SettingsV2TabTypes.IndexingPolicyTab: case SettingsV2TabTypes.IndexingPolicyTab:
return t(Keys.controls.settings.tabTitles.indexingPolicy); return t(Keys.controls.settings.tabTitles.indexingPolicy);
case SettingsV2TabTypes.PartitionKeyTab: case SettingsV2TabTypes.PartitionKeyTab:
return isFabricNative() return t(Keys.controls.settings.tabTitles.partitionKeys);
? t(Keys.controls.settings.tabTitles.partitionKeys)
: t(Keys.controls.settings.tabTitles.partitionKeysPreview);
case SettingsV2TabTypes.ComputedPropertiesTab: case SettingsV2TabTypes.ComputedPropertiesTab:
return t(Keys.controls.settings.tabTitles.computedProperties); return t(Keys.controls.settings.tabTitles.computedProperties);
case SettingsV2TabTypes.ContainerVectorPolicyTab: case SettingsV2TabTypes.ContainerVectorPolicyTab:
@@ -429,7 +429,7 @@ exports[`SettingsComponent renders 1`] = `
"data-test": "settings-tab-header/PartitionKeyTab", "data-test": "settings-tab-header/PartitionKeyTab",
} }
} }
headerText="Partition Keys (preview)" headerText="Partition Keys"
itemKey="PartitionKeyTab" itemKey="PartitionKeyTab"
key="PartitionKeyTab" key="PartitionKeyTab"
style={ style={
@@ -670,7 +670,7 @@ exports[`SettingsComponent renders 1`] = `
"data-test": "settings-tab-header/GlobalSecondaryIndexTab", "data-test": "settings-tab-header/GlobalSecondaryIndexTab",
} }
} }
headerText="Global Secondary Index (Preview)" headerText="Global Secondary Index"
itemKey="GlobalSecondaryIndexTab" itemKey="GlobalSecondaryIndexTab"
key="GlobalSecondaryIndexTab" key="GlobalSecondaryIndexTab"
style={ style={
+169
View File
@@ -0,0 +1,169 @@
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 { Capability, 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: [] as Capability[],
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",
capabilities: [] as Capability[],
};
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);
});
});
});
+9 -3
View File
@@ -219,7 +219,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",
@@ -241,7 +245,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,
}, },
@@ -250,7 +254,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)}`);
@@ -1,3 +1,4 @@
import { Capability } from "Contracts/DataModels";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -12,4 +13,58 @@ 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",
capabilities: [] as Capability[],
};
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",
capabilities: [] as Capability[],
};
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();
});
});
}); });
@@ -20,6 +20,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
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 {
@@ -67,6 +68,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 = {
@@ -167,7 +170,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
)} )}
{!this.state.errorMessage && isFreeTierAccount() && ( {!this.state.errorMessage && isFreeTierAccount(this.props.targetAccountOverride) && (
<PanelInfoErrorComponent <PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)} message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info" messageType="info"
@@ -644,53 +647,57 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
)} )}
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && ( {!isServerlessAccount(this.props.targetAccountOverride) &&
<Stack horizontal verticalAlign="center"> !this.state.createNewDatabase &&
<Checkbox this.isSelectedDatabaseSharedThroughput() && (
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, { <Stack horizontal verticalAlign="center">
collectionName: getCollectionName().toLocaleLowerCase(), <Checkbox
})} label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
checked={this.state.enableDedicatedThroughput} collectionName: getCollectionName().toLocaleLowerCase(),
styles={{ })}
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" }, checked={this.state.enableDedicatedThroughput}
checkbox: { width: 12, height: 12 }, styles={{
label: { padding: 0, alignItems: "center" }, text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
root: { checkbox: { width: 12, height: 12 },
selectors: { label: { padding: 0, alignItems: "center" },
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" }, root: {
selectors: {
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" },
},
}, },
}, }}
}} onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => this.setState({ enableDedicatedThroughput: isChecked })
this.setState({ enableDedicatedThroughput: isChecked }) }
} />
/> <TooltipHost
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge}
directionalHint={DirectionalHint.bottomLeftEdge} content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(), collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(), collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})} })}
/> >
</TooltipHost> <Icon
</Stack> iconName="Info"
)} className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
/>
</TooltipHost>
</Stack>
)}
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && ( {this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={
isFreeTierAccount(this.props.targetAccountOverride) && !isFirstResourceCreated
}
isDatabase={false} isDatabase={false}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={isFreeTierAccount()} isFreeTier={isFreeTierAccount(this.props.targetAccountOverride)}
isQuickstart={this.props.isQuickstart} isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)} setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
@@ -767,7 +774,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={this.state.enableAnalyticalStore} checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()} disabled={!isSynapseLinkEnabled(this.props.targetAccountOverride)}
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)} aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
aria-checked={this.state.enableAnalyticalStore} aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
@@ -782,7 +789,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore} checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()} disabled={!isSynapseLinkEnabled(this.props.targetAccountOverride)}
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)} aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
aria-checked={!this.state.enableAnalyticalStore} aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
@@ -796,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div> </div>
</Stack> </Stack>
{!isSynapseLinkEnabled() && ( {!isSynapseLinkEnabled(this.props.targetAccountOverride) && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, { {t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
@@ -814,7 +821,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 } }}
/> />
@@ -865,6 +872,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}> <Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}> <Stack styles={{ root: { paddingLeft: 40 } }}>
<FullTextPoliciesComponent <FullTextPoliciesComponent
targetAccountOverride={this.props.targetAccountOverride}
fullTextPolicy={this.state.fullTextPolicy} fullTextPolicy={this.state.fullTextPolicy}
onFullTextPathChange={( onFullTextPathChange={(
fullTextPolicy: DataModels.FullTextPolicy, fullTextPolicy: DataModels.FullTextPolicy,
@@ -1000,6 +1008,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(),
@@ -1087,6 +1098,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);
@@ -1124,7 +1139,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// } // }
private shouldShowCollectionThroughputInput(): boolean { private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) { if (isServerlessAccount(this.props.targetAccountOverride)) {
return false; return false;
} }
@@ -1140,7 +1155,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private shouldShowIndexingOptionsForFreeTierAccount(): boolean { private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
if (!isFreeTierAccount()) { if (!isFreeTierAccount(this.props.targetAccountOverride)) {
return false; return false;
} }
@@ -1148,7 +1163,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private shouldShowVectorSearchParameters() { private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput()); const targetAccount = this.props.targetAccountOverride;
return (
isVectorSearchEnabled(targetAccount) &&
(isServerlessAccount(targetAccount) || this.shouldShowCollectionThroughputInput())
);
} }
private shouldShowFullTextSearchParameters() { private shouldShowFullTextSearchParameters() {
@@ -1227,7 +1246,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private getAnalyticalStorageTtl(): number { private getAnalyticalStorageTtl(): number {
if (!isSynapseLinkEnabled()) { if (!isSynapseLinkEnabled(this.props.targetAccountOverride)) {
return undefined; return undefined;
} }
@@ -1367,13 +1386,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) {
@@ -1402,7 +1424,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);
@@ -2,6 +2,7 @@ import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
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 { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { Keys, t } from "Localization"; import { Keys, t } from "Localization";
import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { isFabricNative } from "Platform/Fabric/FabricUtil";
@@ -68,7 +69,10 @@ export function getPartitionKey(isQuickstart?: boolean): string {
return ""; return "";
} }
export function isFreeTierAccount(): boolean { export function isFreeTierAccount(targetAccountOverride?: AccountOverride): boolean {
if (targetAccountOverride) {
return targetAccountOverride.enableFreeTier ?? false;
}
return userContext.databaseAccount?.properties?.enableFreeTier; return userContext.databaseAccount?.properties?.enableFreeTier;
} }
@@ -130,7 +134,16 @@ export function AnalyticalStorageContent(): JSX.Element {
); );
} }
export function isSynapseLinkEnabled(): boolean { export function isSynapseLinkEnabled(targetAccountOverride?: AccountOverride): boolean {
if (targetAccountOverride) {
if (targetAccountOverride.enableAnalyticalStorage) {
return true;
}
return targetAccountOverride.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
if (!userContext.databaseAccount) { if (!userContext.databaseAccount) {
return false; return false;
} }
@@ -0,0 +1,218 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { initiateDataTransfer } from "Common/dataAccess/dataTransfers";
import { DatabaseAccount } from "Contracts/DataModels";
import * as ViewModels from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import * as React from "react";
import { updateUserContext } from "UserContext";
import { ChangePartitionKeyPane } from "./ChangePartitionKeyPane";
jest.mock("Common/ErrorHandlingUtils", () => ({
handleError: jest.fn(),
getErrorMessage: jest.fn().mockReturnValue("error"),
getErrorStack: jest.fn().mockReturnValue(""),
}));
jest.mock("Common/dataAccess/createCollection", () => ({
createCollection: jest.fn().mockResolvedValue({}),
}));
jest.mock("Common/dataAccess/readDatabases", () => ({
readDatabases: jest.fn().mockResolvedValue([]),
}));
jest.mock("Common/dataAccess/dataTransfers", () => ({
initiateDataTransfer: jest.fn().mockResolvedValue({}),
}));
jest.mock("Utils/arm/databaseAccountUtils", () => ({
fetchDatabaseAccount: jest.fn().mockResolvedValue(null),
}));
jest.mock("Utils/arm/generatedClients/cosmos/databaseAccounts", () => ({
update: jest.fn().mockResolvedValue({}),
}));
jest.mock("hooks/useSidePanel", () => ({
useSidePanel: {
getState: () => ({
closeSidePanel: jest.fn(),
openSidePanel: jest.fn(),
}),
},
}));
jest.mock("Explorer/useDatabases", () => {
const state: Record<string, unknown> = {
databases: [],
resourceTokenCollection: undefined,
resourceTokenDatabase: undefined,
sampleDataResourceTokenCollection: undefined,
};
const mockStore = Object.assign(
jest.fn(() => state),
{
getState: () => state,
setState: jest.fn(),
subscribe: jest.fn(),
destroy: jest.fn(),
},
);
return { useDatabases: mockStore };
});
jest.mock("Common/LoadingOverlay", () => {
return {
__esModule: true,
default: ({ isLoading, label }: { isLoading: boolean; label: string }) =>
isLoading ? <div data-testid="loading-overlay">{label}</div> : null,
};
});
const createMockCollection = (id: string): ViewModels.Collection => {
const mockCollection = {
id: jest.fn().mockReturnValue(id),
offer: jest.fn().mockReturnValue(undefined),
partitionKey: { kind: "Hash", paths: ["/id"], version: 2 },
partitionKeyProperties: ["id"],
databaseId: "testDb",
} as unknown as ViewModels.Collection;
return mockCollection;
};
const createMockDatabase = (id: string, collections: ViewModels.Collection[] = []): ViewModels.Database => {
return {
id: jest.fn().mockReturnValue(id),
collections: jest.fn().mockReturnValue(collections),
} as unknown as ViewModels.Database;
};
describe("ChangePartitionKeyPane", () => {
const mockExplorer = new Explorer();
const mockOnClose = jest.fn().mockResolvedValue(undefined);
const mockCollection = createMockCollection("testCollection");
const mockDatabase = createMockDatabase("testDb", [mockCollection]);
beforeEach(() => {
jest.clearAllMocks();
updateUserContext({
databaseAccount: {
name: "testAccount",
id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/testAccount",
properties: {
documentEndpoint: "https://test.documents.azure.com",
capabilities: [],
backupPolicy: { type: "Periodic" },
},
} as unknown as DatabaseAccount,
subscriptionId: "sub1",
resourceGroup: "rg1",
apiType: "SQL",
});
});
const renderPane = () => {
return render(
<ChangePartitionKeyPane
sourceDatabase={mockDatabase}
sourceCollection={mockCollection}
explorer={mockExplorer}
onClose={mockOnClose}
/>,
);
};
it("renders migration type choice group", () => {
renderPane();
expect(screen.getByText("Migration type")).toBeTruthy();
expect(screen.getByText("Offline mode")).toBeTruthy();
expect(screen.getByText("Online mode")).toBeTruthy();
});
it("defaults to offline migration type", () => {
renderPane();
const offlineRadio = screen.getByRole("radio", { name: "Offline mode" }) as HTMLInputElement;
expect(offlineRadio.checked).toBe(true);
});
it("does not show online prerequisites section when offline is selected", () => {
const { container } = renderPane();
expect(container.querySelector("[data-test='online-prerequisites-section']")).toBeNull();
});
it("shows online prerequisites section when online is selected", () => {
renderPane();
const onlineRadio = screen.getByRole("radio", { name: "Online mode" });
fireEvent.click(onlineRadio);
expect(screen.getByText("Online container copy")).toBeTruthy();
expect(screen.getByText("Point In Time Restore enabled")).toBeTruthy();
expect(screen.getByText("Online copy enabled")).toBeTruthy();
});
it("shows prerequisite sections when online prerequisites are not met", () => {
renderPane();
const onlineRadio = screen.getByRole("radio", { name: "Online mode" });
fireEvent.click(onlineRadio);
// When prerequisites aren't met, the enable buttons should be visible
expect(screen.getByText("Enable Point In Time Restore")).toBeTruthy();
expect(screen.getAllByRole("button", { name: "Enable Online Copy" }).length).toBeGreaterThan(0);
});
it("shows enable PITR button when PITR is not enabled", () => {
renderPane();
const onlineRadio = screen.getByRole("radio", { name: "Online mode" });
fireEvent.click(onlineRadio);
expect(screen.getByText("Enable Point In Time Restore")).toBeTruthy();
});
it("does not show PITR enable button when PITR is already enabled", () => {
updateUserContext({
databaseAccount: {
name: "testAccount",
id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/testAccount",
properties: {
documentEndpoint: "https://test.documents.azure.com",
capabilities: [],
backupPolicy: { type: "Continuous" },
},
} as unknown as DatabaseAccount,
});
renderPane();
const onlineRadio = screen.getByRole("radio", { name: "Online mode" });
fireEvent.click(onlineRadio);
expect(screen.queryByText("Enable Point In Time Restore")).toBeNull();
});
it("disables online copy button when PITR is not enabled", () => {
renderPane();
const onlineRadio = screen.getByRole("radio", { name: "Online mode" });
fireEvent.click(onlineRadio);
const enableOnlineCopyBtns = screen.getAllByRole("button", { name: "Enable Online Copy" });
expect(enableOnlineCopyBtns.length).toBeGreaterThan(0);
expect((enableOnlineCopyBtns[0] as HTMLButtonElement).disabled).toBe(true);
});
it("passes mode to initiateDataTransfer when submitting", async () => {
const mockInitiateDataTransfer = jest.mocked(initiateDataTransfer);
// Mock refreshAllDatabases on the prototype to catch all calls
const refreshSpy = jest.spyOn(Explorer.prototype, "refreshAllDatabases").mockResolvedValue();
const { container } = renderPane();
// Fill in partition key (required for createContainer — state starts undefined)
const partitionKeyInput = container.querySelector("#addCollection-partitionKeyValue") as HTMLInputElement;
expect(partitionKeyInput).not.toBeNull();
fireEvent.change(partitionKeyInput, { target: { value: "/myKey" } });
const form = container.querySelector("form");
expect(form).not.toBeNull();
await act(async () => {
fireEvent.submit(form!);
});
expect(mockInitiateDataTransfer).toHaveBeenCalled();
expect(mockInitiateDataTransfer.mock.calls[0][0].mode).toBe("Offline");
refreshSpy.mockRestore();
});
});
@@ -7,16 +7,24 @@ import {
IconButton, IconButton,
Link, Link,
MessageBar, MessageBar,
MessageBarType,
PrimaryButton,
Stack, Stack,
Text, Text,
TooltipHost, TooltipHost,
} from "@fluentui/react"; } from "@fluentui/react";
import MarkdownRender from "@nteract/markdown";
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { CapabilityNames } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import LoadingOverlay from "Common/LoadingOverlay";
import { logError } from "Common/Logger";
import { createCollection } from "Common/dataAccess/createCollection"; import { createCollection } from "Common/dataAccess/createCollection";
import { DataTransferParams, initiateDataTransfer } from "Common/dataAccess/dataTransfers"; import { DataTransferParams, initiateDataTransfer } from "Common/dataAccess/dataTransfers";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import * as ViewModels from "Contracts/ViewModels"; import * as ViewModels from "Contracts/ViewModels";
import { buildResourceLink } from "Explorer/ContainerCopy/CopyJobUtils";
import { BackupPolicyType } from "Explorer/ContainerCopy/Enums/CopyJobEnums";
import { import {
getPartitionKeyName, getPartitionKeyName,
getPartitionKeyPlaceHolder, getPartitionKeyPlaceHolder,
@@ -30,6 +38,8 @@ import { Keys, t } from "Localization";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils"; import { getCollectionName } from "Utils/APITypeUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils"; import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { fetchDatabaseAccount } from "Utils/arm/databaseAccountUtils";
import { update as updateDatabaseAccount } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import * as React from "react"; import * as React from "react";
@@ -40,6 +50,15 @@ export interface ChangePartitionKeyPaneProps {
onClose: () => Promise<void>; onClose: () => Promise<void>;
} }
const checkPitrEnabled = (account: DataModels.DatabaseAccount): boolean => {
return account?.properties?.backupPolicy?.type === BackupPolicyType.Continuous;
};
const checkOnlineCopyEnabled = (account: DataModels.DatabaseAccount): boolean => {
const capabilities = account?.properties?.capabilities ?? [];
return capabilities.some((cap) => cap.name === CapabilityNames.EnableOnlineCopyFeature);
};
export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
sourceDatabase, sourceDatabase,
sourceCollection, sourceCollection,
@@ -52,6 +71,118 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
const [isExecuting, setIsExecuting] = React.useState<boolean>(false); const [isExecuting, setIsExecuting] = React.useState<boolean>(false);
const [subPartitionKeys, setSubPartitionKeys] = React.useState<string[]>([]); const [subPartitionKeys, setSubPartitionKeys] = React.useState<string[]>([]);
const [partitionKey, setPartitionKey] = React.useState<string>(); const [partitionKey, setPartitionKey] = React.useState<string>();
const [onlineMode, setOnlineMode] = React.useState<boolean>(false);
// Pane-local account state for tracking prerequisite enablement
const [localAccount, setLocalAccount] = React.useState<DataModels.DatabaseAccount>(userContext.databaseAccount);
const [isEnablingPrerequisite, setIsEnablingPrerequisite] = React.useState<boolean>(false);
const [prerequisiteLoaderMessage, setPrerequisiteLoaderMessage] = React.useState<string>("");
const pitrEnabled = checkPitrEnabled(localAccount);
const onlineCopyFeatureEnabled = checkOnlineCopyEnabled(localAccount);
const onlinePrerequisitesMet = pitrEnabled && onlineCopyFeatureEnabled;
const accountName = localAccount?.name ?? "";
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const intervalRef = React.useRef<NodeJS.Timeout | null>(null);
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
React.useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const refreshAccount = async (): Promise<DataModels.DatabaseAccount | null> => {
try {
const account = await fetchDatabaseAccount(subscriptionId, resourceGroup, accountName);
if (account) {
setLocalAccount(account);
}
return account;
} catch (error) {
logError(
error.message || "Error fetching account after enabling prerequisite.",
"ChangePartitionKey/refreshAccount",
);
return null;
}
};
const clearPollingTimers = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
const startPollingForAccountUpdate = () => {
intervalRef.current = setInterval(() => {
refreshAccount();
}, 30 * 1000);
timeoutRef.current = setTimeout(
() => {
clearPollingTimers();
setIsEnablingPrerequisite(false);
},
10 * 60 * 1000,
);
};
const handleEnablePitr = () => {
const sourceAccountLink = buildResourceLink(localAccount);
const featureUrl = `${sourceAccountLink}/backupRestore`;
setIsEnablingPrerequisite(true);
setPrerequisiteLoaderMessage(t(Keys.containerCopy.popoverOverlaySpinnerLabel));
window.open(featureUrl, "_blank");
startPollingForAccountUpdate();
};
const handleEnableOnlineCopy = async () => {
setIsEnablingPrerequisite(true);
try {
setPrerequisiteLoaderMessage(
t(Keys.containerCopy.onlineCopyEnabled.validateAllVersionsAndDeletesChangeFeedSpinnerLabel),
);
const currentAccount = await fetchDatabaseAccount(subscriptionId, resourceGroup, accountName);
if (!currentAccount?.properties?.enableAllVersionsAndDeletesChangeFeed) {
setPrerequisiteLoaderMessage(
t(Keys.containerCopy.onlineCopyEnabled.enablingAllVersionsAndDeletesChangeFeedSpinnerLabel),
);
await updateDatabaseAccount(subscriptionId, resourceGroup, accountName, {
properties: {
enableAllVersionsAndDeletesChangeFeed: true,
},
});
}
const capabilities = currentAccount?.properties?.capabilities ?? [];
setPrerequisiteLoaderMessage(
t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { accountName }),
);
await updateDatabaseAccount(subscriptionId, resourceGroup, accountName, {
properties: {
capabilities: [...capabilities, { name: CapabilityNames.EnableOnlineCopyFeature }],
},
});
startPollingForAccountUpdate();
} catch (error) {
logError(error.message || "Failed to enable online copy feature.", "ChangePartitionKey/handleEnableOnlineCopy");
setFormError("Failed to enable online copy feature. Please try again.");
setIsEnablingPrerequisite(false);
}
};
const getCollectionOptions = (): IDropdownOption[] => { const getCollectionOptions = (): IDropdownOption[] => {
return sourceDatabase return sourceDatabase
@@ -84,9 +215,17 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
setFormError("Choose an existing container"); setFormError("Choose an existing container");
return false; return false;
} }
if (onlineMode && !onlinePrerequisitesMet) {
setFormError("Online migration prerequisites must be enabled before proceeding.");
return false;
}
return true; return true;
}; };
const getModeForApi = (): "Offline" | "Online" => {
return onlineMode ? "Online" : "Offline";
};
const createDataTransferJob = async () => { const createDataTransferJob = async () => {
const jobName = `Portal_${targetCollectionId}_${Math.floor(Date.now() / 1000)}`; const jobName = `Portal_${targetCollectionId}_${Math.floor(Date.now() / 1000)}`;
const dataTransferParams: DataTransferParams = { const dataTransferParams: DataTransferParams = {
@@ -99,6 +238,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
sourceCollectionName: sourceCollection.id(), sourceCollectionName: sourceCollection.id(),
targetDatabaseName: sourceDatabase.id(), targetDatabaseName: sourceDatabase.id(),
targetCollectionName: targetCollectionId, targetCollectionName: targetCollectionId,
mode: getModeForApi(),
}; };
await initiateDataTransfer(dataTransferParams); await initiateDataTransfer(dataTransferParams);
}; };
@@ -133,12 +273,18 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
return !!selectedDatabase?.offer(); return !!selectedDatabase?.offer();
}; };
const isSubmitDisabled = onlineMode && !onlinePrerequisitesMet;
const migrationTypeLowercase = getModeForApi().toLowerCase() as keyof typeof Keys.containerCopy.migrationType;
const migrationTypeContent = Keys.containerCopy.migrationType[migrationTypeLowercase];
return ( return (
<RightPaneForm <RightPaneForm
formError={formError} formError={formError}
isExecuting={isExecuting} isExecuting={isExecuting}
onSubmit={submit} onSubmit={submit}
submitButtonText={t(Keys.common.ok)} submitButtonText={t(Keys.common.ok)}
isSubmitButtonDisabled={isSubmitDisabled}
> >
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent"> <Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
@@ -151,11 +297,58 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
{t(Keys.common.learnMore)} {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
{/* Migration Type */}
<Stack data-test="migration-type-section">
<Text className="panelTextBold" variant="small" style={{ marginBottom: 4 }}>
{t(Keys.containerCopy.migrationTypeTitle)}
</Text>
<Stack className="panelGroupSpacing" horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={!onlineMode}
aria-label="Offline mode"
aria-checked={!onlineMode}
name="migrationType"
type="radio"
role="radio"
id="migrationTypeOffline"
tabIndex={0}
onChange={() => setOnlineMode(false)}
/>
<span className="panelRadioBtnLabel">{t(Keys.containerCopy.migrationType.offline.title)}</span>
<input
className="panelRadioBtn"
checked={onlineMode}
aria-label="Online mode"
aria-checked={onlineMode}
name="migrationType"
type="radio"
role="radio"
tabIndex={0}
onChange={() => setOnlineMode(true)}
/>
<span className="panelRadioBtnLabel">{t(Keys.containerCopy.migrationType.online.title)}</span>
</div>
</Stack>
{migrationTypeContent && (
<Text
variant="small"
style={{ color: "var(--colorNeutralForeground1)", marginTop: 4 }}
data-test={`migration-type-description-${migrationTypeLowercase}`}
>
<MarkdownRender source={t(migrationTypeContent.description)} linkTarget="_blank" />
</Text>
)}
</Stack>
<Stack> <Stack>
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Database id {t(Keys.panes.addDatabase.databaseIdLabel)}
</Text> </Text>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
@@ -420,6 +613,89 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
/> />
</Stack> </Stack>
)} )}
{/* Online prerequisites section */}
{onlineMode && (
<Stack data-test="online-prerequisites-section" tokens={{ childrenGap: 10 }}>
<LoadingOverlay isLoading={isEnablingPrerequisite} label={prerequisiteLoaderMessage} />
<Text className="panelTextBold" variant="small">
{t(Keys.containerCopy.assignPermissions.onlineConfiguration.title)}
</Text>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.containerCopy.assignPermissions.onlineConfiguration.description, { accountName })}
</Text>
{/* Point In Time Restore */}
<Stack tokens={{ childrenGap: 5 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 5 }}>
<Icon
iconName={pitrEnabled ? "SkypeCircleCheck" : "StatusCircleRing"}
styles={{
root: { color: pitrEnabled ? "green" : "var(--colorNeutralForeground1)", fontSize: 16 },
}}
/>
<Text variant="small" style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
{t(Keys.containerCopy.pointInTimeRestore.title)}
</Text>
</Stack>
{!pitrEnabled && (
<Stack tokens={{ childrenGap: 10, padding: "0 0 0 20px" }}>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.containerCopy.pointInTimeRestore.description, { accessName: accountName })}
</Text>
<PrimaryButton
data-test="enable-pitr-button"
text={t(Keys.containerCopy.pointInTimeRestore.buttonText)}
disabled={isEnablingPrerequisite}
onClick={handleEnablePitr}
styles={{ root: { width: "fit-content" } }}
/>
</Stack>
)}
</Stack>
{/* Online Copy Enabled */}
<Stack tokens={{ childrenGap: 5 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 5 }}>
<Icon
iconName={onlineCopyFeatureEnabled ? "SkypeCircleCheck" : "StatusCircleRing"}
styles={{
root: {
color: onlineCopyFeatureEnabled ? "green" : "var(--colorNeutralForeground1)",
fontSize: 16,
},
}}
/>
<Text variant="small" style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
{t(Keys.containerCopy.onlineCopyEnabled.title)}
</Text>
</Stack>
{!onlineCopyFeatureEnabled && (
<Stack tokens={{ childrenGap: 10, padding: "0 0 0 20px" }}>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.containerCopy.onlineCopyEnabled.description, { accountName })}&ensp;
<Link href={t(Keys.containerCopy.onlineCopyEnabled.href)} target="_blank" rel="noopener noreferrer">
{t(Keys.containerCopy.onlineCopyEnabled.hrefText)}
</Link>
</Text>
<PrimaryButton
data-test="enable-online-copy-button"
text={t(Keys.containerCopy.onlineCopyEnabled.buttonText)}
disabled={isEnablingPrerequisite || !pitrEnabled}
onClick={handleEnableOnlineCopy}
styles={{ root: { width: "fit-content" } }}
/>
</Stack>
)}
</Stack>
{!onlinePrerequisitesMet && (
<MessageBar messageBarType={MessageBarType.warning} data-test="online-prerequisites-warning">
{t(Keys.containerCopy.onlineCopyEnabled.onlineMigrationPrerequisitesMessage)}
</MessageBar>
)}
</Stack>
)}
</Stack> </Stack>
</RightPaneForm> </RightPaneForm>
); );
+72 -13
View File
@@ -6,18 +6,21 @@ import { render } from "react-dom";
import ChevronRight from "../images/chevron-right.svg"; import ChevronRight from "../images/chevron-right.svg";
import "../less/hostedexplorer.less"; import "../less/hostedexplorer.less";
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
import { logError } from "./Common/Logger";
import { DatabaseAccount } from "./Contracts/DataModels"; import { DatabaseAccount } from "./Contracts/DataModels";
import "./Explorer/Menus/NavBar/MeControlComponent.less"; import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; import { ConnectExplorer, fetchEncryptedToken } from "./Platform/Hosted/Components/ConnectExplorer";
import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel"; import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel";
import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton";
import { MeControl } from "./Platform/Hosted/Components/MeControl"; import { MeControl } from "./Platform/Hosted/Components/MeControl";
import { SignInButton } from "./Platform/Hosted/Components/SignInButton"; import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
import "./Platform/Hosted/ConnectScreen.less"; import "./Platform/Hosted/ConnectScreen.less";
import { isResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { allowedHostedExplorerEndpoints } from "./Utils/EndpointUtils";
import { useAADAuth } from "./hooks/useAADAuth"; import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useTokenMetadata } from "./hooks/usePortalAccessToken"; import { useTokenMetadata } from "./hooks/usePortalAccessToken";
@@ -42,20 +45,68 @@ const App: React.FunctionComponent = () => {
const ref = React.useRef<HTMLIFrameElement>(); const ref = React.useRef<HTMLIFrameElement>();
const connectWithConnectionString = React.useCallback(
(connStr: string) => {
if (!connStr || authType) {
return;
}
setConnectionString(connStr);
if (isResourceTokenConnectionString(connStr)) {
setAuthType(AuthType.ResourceToken);
} else {
fetchEncryptedToken(connStr)
.then((token) => {
setEncryptedToken(token);
setAuthType(AuthType.ConnectionString);
})
.catch((error) => {
logError(
`Failed to connect with connection string: ${error}`,
"HostedExplorer/connectWithConnectionString",
);
});
}
},
[authType],
);
// Listen for connection string sent via postMessage (from TryCosmosDB)
React.useEffect(() => {
const MSG_READY = "tryCosmosDBReady";
const MSG_CONNECTION_STRING = "tryCosmosDBConnectionString";
// Signal to the opener that we are ready to receive a connection string
if (window.opener) {
try {
for (const origin of allowedHostedExplorerEndpoints) {
window.opener.postMessage({ type: MSG_READY }, origin);
}
} catch {
// opener may be cross-origin, ignore
}
}
const handler = (event: MessageEvent) => {
if (!allowedHostedExplorerEndpoints.includes(event.origin)) {
return;
}
if (event.data?.type === MSG_CONNECTION_STRING && event.data?.connectionString) {
connectWithConnectionString(event.data.connectionString);
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [connectWithConnectionString]);
React.useEffect(() => { React.useEffect(() => {
// If ref.current is undefined no iframe has been rendered // If ref.current is undefined no iframe has been rendered
if (ref.current) { if (ref.current) {
// In hosted mode, we can set global properties directly on the child iframe. // In hosted mode, we can set global properties directly on the child iframe.
// This is not possible in the portal where the iframes have different origins // This is not possible in the portal where the iframes have different origins
const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame; const frameWindow = ref.current.contentWindow as HostedExplorerChildFrame;
// AAD authenticated uses ALWAYS using AAD authType
if (isLoggedIn) { if (authType === AuthType.EncryptedToken) {
frameWindow.hostedConfig = {
authType: AuthType.AAD,
databaseAccount,
authorizationToken: armToken,
};
} else if (authType === AuthType.EncryptedToken) {
frameWindow.hostedConfig = { frameWindow.hostedConfig = {
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,
encryptedToken, encryptedToken,
@@ -73,12 +124,18 @@ const App: React.FunctionComponent = () => {
authType: AuthType.ResourceToken, authType: AuthType.ResourceToken,
resourceToken: connectionString, resourceToken: connectionString,
}; };
} else if (isLoggedIn && !connectionString) {
frameWindow.hostedConfig = {
authType: AuthType.AAD,
databaseAccount,
authorizationToken: armToken,
};
} }
} }
}); });
const showExplorer = const showExplorer =
(config && isLoggedIn && databaseAccount) || (config && isLoggedIn && databaseAccount && !connectionString) ||
(encryptedTokenMetadata && encryptedTokenMetadata) || (encryptedTokenMetadata && encryptedTokenMetadata) ||
(authType === AuthType.ResourceToken && connectionString); (authType === AuthType.ResourceToken && connectionString);
@@ -99,12 +156,12 @@ const App: React.FunctionComponent = () => {
{(isLoggedIn || encryptedTokenMetadata?.accountName) && ( {(isLoggedIn || encryptedTokenMetadata?.accountName) && (
<img className="chevronRight" src={ChevronRight} alt="account separator" /> <img className="chevronRight" src={ChevronRight} alt="account separator" />
)} )}
{isLoggedIn && ( {isLoggedIn && !connectionString && (
<span className="accountSwitchComponentContainer"> <span className="accountSwitchComponentContainer">
<AccountSwitcher armToken={armToken} setDatabaseAccount={setDatabaseAccount} /> <AccountSwitcher armToken={armToken} setDatabaseAccount={setDatabaseAccount} />
</span> </span>
)} )}
{!isLoggedIn && encryptedTokenMetadata?.accountName && ( {(!isLoggedIn || connectionString) && encryptedTokenMetadata?.accountName && (
<span className="accountSwitchComponentContainer"> <span className="accountSwitchComponentContainer">
<span className="accountNameHeader">{encryptedTokenMetadata?.accountName}</span> <span className="accountNameHeader">{encryptedTokenMetadata?.accountName}</span>
</span> </span>
@@ -127,7 +184,9 @@ const App: React.FunctionComponent = () => {
// It's possible this can be changed once all knockout code has been removed. // It's possible this can be changed once all knockout code has been removed.
<iframe <iframe
// Setting key is needed so React will re-render this element on any account change // Setting key is needed so React will re-render this element on any account change
key={databaseAccount?.id || encryptedTokenMetadata?.accountName || authType} key={
authType ? `${authType}-${encryptedTokenMetadata?.accountName || connectionString}` : databaseAccount?.id
}
ref={ref} ref={ref}
data-test="DataExplorerFrame" data-test="DataExplorerFrame"
id="explorerMenu" id="explorerMenu"
+168 -7
View File
@@ -34,6 +34,8 @@
"browse": "Procházet", "browse": "Procházet",
"increaseValueBy1": "Zvýšit hodnotu o 1", "increaseValueBy1": "Zvýšit hodnotu o 1",
"decreaseValueBy1": "Snížit hodnotu o 1", "decreaseValueBy1": "Snížit hodnotu o 1",
"on": "Zapnuto",
"off": "Vypnuto",
"preview": "Preview" "preview": "Preview"
}, },
"splashScreen": { "splashScreen": {
@@ -76,7 +78,7 @@
"description": "Vytvoření tabulky a interakce s daty pomocí rozhraní prostředí PostgreSQL" "description": "Vytvoření tabulky a interakce s daty pomocí rozhraní prostředí PostgreSQL"
}, },
"vcoreMongo": { "vcoreMongo": {
"title": "Prostředí Mongo", "title": "Mongo Shell",
"description": "Vytvořte kolekci a pracujte s daty pomocí rozhraní prostředí MongoDB" "description": "Vytvořte kolekci a pracujte s daty pomocí rozhraní prostředí MongoDB"
} }
}, },
@@ -414,7 +416,7 @@
"refreshGridFailed": "Nepovedlo se aktualizovat mřížku dokumentů" "refreshGridFailed": "Nepovedlo se aktualizovat mřížku dokumentů"
}, },
"mongoShell": { "mongoShell": {
"title": "Prostředí Mongo" "title": "Mongo Shell"
} }
}, },
"panes": { "panes": {
@@ -762,7 +764,7 @@
"computedProperties": "Vypočítané vlastnosti", "computedProperties": "Vypočítané vlastnosti",
"containerPolicies": "Zásady kontejneru", "containerPolicies": "Zásady kontejneru",
"throughputBuckets": "Kbelíky propustnosti", "throughputBuckets": "Kbelíky propustnosti",
"globalSecondaryIndexPreview": "Globální sekundární index (Preview)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "Zásady maskování" "maskingPolicyPreview": "Zásady maskování"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -795,7 +797,7 @@
"perMonth": "/měs." "perMonth": "/měs."
}, },
"throughput": { "throughput": {
"manualToAutoscaleDisclaimer": "Počáteční maximální hodnota RU/s pro automatické škálování bude určena systémem na základě nastavení aktuální manuální propustnosti a úložiště vašeho prostředku. Po povolení automatického škálování můžete změnit maximální počet RU/s.", "manualToAutoscaleDisclaimer": "Počáteční maximální hodnota RU/s pro automatické škálování bude určena systémem na základě nastavení aktuální ruční propustnosti a úložiště vašeho prostředku. Po povolení automatického škálování můžete změnit maximální počet RU/s.",
"ttlWarningText": "Systém bude položky automaticky odstraňovat na základě hodnoty TTL (v sekundách), kterou zadáte, aniž by klientská aplikace musela výslovně provést operaci odstranění. Další informace viz", "ttlWarningText": "Systém bude položky automaticky odstraňovat na základě hodnoty TTL (v sekundách), kterou zadáte, aniž by klientská aplikace musela výslovně provést operaci odstranění. Další informace viz",
"ttlWarningLinkText": "Hodnota TTL (Time to Live) v Azure Cosmos DB", "ttlWarningLinkText": "Hodnota TTL (Time to Live) v Azure Cosmos DB",
"unsavedIndexingPolicy": "zásada indexování", "unsavedIndexingPolicy": "zásada indexování",
@@ -813,8 +815,8 @@
"saveThroughputWarning": "Změna nastavení propustnosti ovlivní výši vaší faktury. Před uložením změn si prosím projděte aktualizovaný odhad nákladů uvedený níže.", "saveThroughputWarning": "Změna nastavení propustnosti ovlivní výši vaší faktury. Před uložením změn si prosím projděte aktualizovaný odhad nákladů uvedený níže.",
"currentAutoscaleThroughput": "Aktuální propustnost automatického škálování:", "currentAutoscaleThroughput": "Aktuální propustnost automatického škálování:",
"targetAutoscaleThroughput": "Cílová propustnost automatického škálování:", "targetAutoscaleThroughput": "Cílová propustnost automatického škálování:",
"currentManualThroughput": "Aktuální manuální propustnost:", "currentManualThroughput": "Aktuální ruční propustnost:",
"targetManualThroughput": "Cílová manuální propustnost:", "targetManualThroughput": "Cílová ruční propustnost:",
"applyDelayedMessage": "Žádost o zvýšení propustnosti se úspěšně odeslala. Dokončení této operace bude trvat 1 až 3 pracovní dny. Nejnovější stav najdete v části Oznámení.", "applyDelayedMessage": "Žádost o zvýšení propustnosti se úspěšně odeslala. Dokončení této operace bude trvat 1 až 3 pracovní dny. Nejnovější stav najdete v části Oznámení.",
"databaseLabel": "Databáze:", "databaseLabel": "Databáze:",
"containerLabel": "Kontejner:", "containerLabel": "Kontejner:",
@@ -947,7 +949,7 @@
"instant": "Okamžité", "instant": "Okamžité",
"fourToSixHrs": "46 hodin", "fourToSixHrs": "46 hodin",
"autoscaleDescription": "Na základě využití se vaše propustnost {{resourceType}} bude škálovat od", "autoscaleDescription": "Na základě využití se vaše propustnost {{resourceType}} bude škálovat od",
"freeTierWarning": "Fakturace začne, pokud zřídíte více než {{ru}} RU/s manuální propustnosti nebo pokud se prostředek při automatickém škálování bude škálovat nad {{ru}} RU/s.", "freeTierWarning": "Fakturace začne, pokud zřídíte více než {{ru}} RU/s ruční propustnosti nebo pokud se prostředek při automatickém škálování bude škálovat nad {{ru}} RU/s.",
"capacityCalculator": "Odhadněte požadovanou hodnotu RU/s pomocí", "capacityCalculator": "Odhadněte požadovanou hodnotu RU/s pomocí",
"capacityCalculatorLink": " kalkulačka kapacity", "capacityCalculatorLink": " kalkulačka kapacity",
"fixedStorageNote": "Při použití kolekce s pevnou kapacitou úložiště můžete nastavit až 10 000 RU/s.", "fixedStorageNote": "Při použití kolekce s pevnou kapacitou úložiště můžete nastavit až 10 000 RU/s.",
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "Velikost kvantování v bajtech musí být větší než 0 a menší nebo rovna 512.", "quantizationByteSizeRangeError": "Velikost kvantování v bajtech musí být větší než 0 a menší nebo rovna 512.",
"indexingSearchListSizeRangeError": "Velikost seznamu prohledávání indexu musí být větší nebo rovna 25 a menší nebo rovna 500." "indexingSearchListSizeRangeError": "Velikost seznamu prohledávání indexu musí být větší nebo rovna 25 a menší nebo rovna 500."
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Zpětná vazba",
"feedbackButtonAriaLabel": "Poskytnout zpětnou vazbu k úlohám kopírování",
"refreshButtonAriaLabel": "Aktualizovat úlohy kopírování",
"createCopyJobButtonLabel": "Vytvořit úlohu kopírování",
"createCopyJobButtonAriaLabel": "Vytvořit novou úlohu kopírování kontejneru"
},
"noCopyJobs": {
"title": "Žádné úlohy kopírování k zobrazení",
"createCopyJobButtonText": "Vytvořit úlohu kopírování kontejneru"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Podrobnosti úlohy",
"errorTitle": "Podrobnosti o chybě",
"selectedContainers": "Vybrané kontejnery"
},
"createCopyJob": {
"panelTitle": "Vytvořit úlohu kopírování"
},
"selectAccount": {
"description": "Vyberte cílový účet, do kterého chcete kopírovat.",
"subscriptionDropdownLabel": "Předplatné",
"subscriptionDropdownPlaceholder": "Vyberte předplatné",
"accountDropdownLabel": "Účet",
"accountDropdownPlaceholder": "Vybrat účet"
},
"migrationType": {
"offline": {
"title": "Offline režim",
"description": "Úlohy offline kopírování kontejnerů umožňují kopírovat data ze zdrojového kontejneru do cílového kontejneru Cosmos DB pro podporovaná rozhraní API. Pro zajištění integrity dat mezi zdrojem a cílem doporučujeme před vytvořením úlohy kopírování zastavit aktualizace zdrojového kontejneru. Další informace o [úlohách offline kopírování](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql)."
},
"online": {
"title": "Online režim",
"description": "Úlohy online kopírování kontejnerů umožňují kopírovat data ze zdrojového kontejneru do cílového kontejneru rozhraní API NoSQL služby Cosmos DB pomocí kanálu změn [Všechny verze a odstranění](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Díky tomu můžou aktualizace ve zdroji pokračovat i během kopírování dat. Na konci je potřeba krátký výpadek, aby bylo možné bezpečně přepnout klientské aplikace na cílový kontejner. Další informace o [úlohách online kopírování](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
"selectContainers": {
"description": "Vyberte zdrojový a cílový kontejner, do kterého chcete kopírovat.",
"sourceContainerSubHeading": "Zdrojový kontejner",
"targetContainerSubHeading": "Cílový kontejner",
"databaseDropdownLabel": "Databáze",
"databaseDropdownPlaceholder": "Vyberte databázi",
"containerDropdownLabel": "Kontejner",
"containerDropdownPlaceholder": "Vybrat kontejner",
"createNewContainerSubHeading": "Nakonfigurujte vlastnosti nového kontejneru v cílovém účtu {{accountName}}.",
"createNewContainerSubHeadingDefault": "Nakonfigurujte vlastnosti nového kontejneru.",
"createContainerButtonLabel": "Vytvořit nový kontejner",
"createContainerHeading": "Vytvořit nový kontejner"
},
"preview": {
"jobNameLabel": "Název úlohy",
"subscriptionLabel": "Cílové předplatné",
"accountLabel": "Cílový účet",
"sourceDatabaseLabel": "Zdrojová databáze",
"sourceContainerLabel": "Zdrojový kontejner",
"targetDatabaseLabel": "Cílová databáze",
"targetContainerLabel": "Cílový kontejner"
},
"assignPermissions": {
"crossAccountDescription": "Pokud chcete zkopírovat data ze zdrojového do cílového kontejneru, podle následujících kroků zajistěte, aby spravovaná identita zdrojového účtu měla přístup ke čtení a zápisu k cílovému účtu.",
"intraAccountOnlineDescription": "Podle následujících kroků povolte online kopírování pro účet {{accountName}}.",
"crossAccountConfiguration": {
"title": "Kopírování kontejnerů mezi účty",
"description": "Podle pokynů níže udělte potřebná oprávnění ke kopírování dat z {{sourceAccount}} do {{destinationAccount}}."
},
"onlineConfiguration": {
"title": "Online kopírování kontejneru",
"description": "Podle pokynů níže povolte online kopírování pro účet {{accountName}}."
}
},
"popoverOverlaySpinnerLabel": "Počkejte prosím, než zpracujeme váš požadavek...",
"addManagedIdentity": {
"title": "Spravovaná identita přiřazená systémem je povolená.",
"description": "Spravovaná identita přiřazená systémem je omezená na jednu na prostředek a je svázaná s životním cyklem tohoto prostředku. Po povolení můžete spravované identitě udělit oprávnění pomocí řízení přístupu na základě role Azure (Azure RBAC). Spravovaná identita se ověřuje pomocí Microsoft Entra ID, abyste žádné přihlašovací údaje nemuseli ukládat do kódu.",
"descriptionHrefText": "Další informace o spravovaných identitách.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "Spravovaná identita přiřazená systémem",
"tooltipContent": "Přečtěte si další informace o",
"tooltipHrefText": "Spravované identity.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "Můžete vybrat existující identitu přiřazenou uživatelem nebo vytvořit novou.",
"userAssignedIdentityLabel": "Můžete také vybrat spravovanou identitu přiřazenou uživatelem.",
"createUserAssignedIdentityLink": "Vytvořit spravovanou identitu přiřazenou uživatelem",
"enablementTitle": "Povolit spravovanou identitu přiřazenou systémem",
"enablementDescription": "Povolte spravovanou identitu přiřazenou systémem pro {{accountName}}. Potvrďte kliknutím na tlačítko Ano."
},
"defaultManagedIdentity": {
"title": "Spravovaná identita přiřazená systémem je nastavená jako výchozí.",
"description": "Zapněte spravovanou identitu přiřazenou systémem a nastavte ji jako výchozí pro {{accountName}}.",
"tooltipContent": "Přečtěte si další informace o",
"tooltipHrefText": "Výchozí spravované identity.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "Spravovaná identita přiřazená systémem nastavená jako výchozí",
"popoverDescription": "Nastavte spravovanou identitu přiřazenou systémem jako výchozí pro {{accountName}}. Potvrďte kliknutím na tlačítko Ano. "
},
"readWritePermissionAssigned": {
"title": "Výchozí identitě byla přiřazena oprávnění ke čtení a zápisu.",
"description": "Pokud chcete povolit kopírování dat ze zdrojového do cílového kontejneru, udělte výchozí identitě zdrojového účtu přístup ke čtení a zápisu v cílovém účtu.",
"tooltipContent": "Přečtěte si další informace o",
"tooltipHrefText": "Oprávnění ke čtení a zápisu.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Přiřaďte výchozí identitě oprávnění ke čtení a zápisu.",
"popoverDescription": "Přiřaďte výchozí identitě zdrojového účtu oprávnění ke čtení a zápisu v cílovém účtu. Potvrďte kliknutím na tlačítko Ano."
},
"pointInTimeRestore": {
"title": "Obnovení k určitému bodu v čase je povolené",
"description": "Aby bylo možné používat úlohy online kopírování kontejnerů, aktualizujte zásady zálohování účtu {{accessName}} z pravidelného na průběžné zálohování. Pro tuto funkci je potřeba povolit průběžné zálohování.",
"tooltipContent": "Přečtěte si další informace o",
"tooltipHrefText": "Průběžné zálohování",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "Povolit obnovení k určitému bodu v čase"
},
"onlineCopyEnabled": {
"title": "Online kopírování je povolené",
"description": "Povolte online kopírování kontejneru kliknutím na tlačítko níže v účtu {{accountName}}.",
"hrefText": "Další informace o úlohách online kopírování",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "Povolit online kopírování",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Ověřuje se režim kanálu změn Všechny verze a odstranění (Preview)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Povoluje se režim kanálu změn Všechny verze a odstranění (Preview)...",
"enablingOnlineCopySpinnerLabel": "Povoluje se online kopírování pro účet {{accountName}}..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "Datum a čas",
"name": "Název úlohy",
"status": "Stav",
"completionPercentage": "Dokončení v %",
"duration": "Doba trvání",
"error": "Chybová zpráva",
"mode": "Režim",
"actions": "Akce"
},
"actions": {
"pause": "Pozastavit",
"resume": "Obnovit",
"complete": "Dokončeno",
"viewDetails": "Zobrazit podrobnosti"
},
"status": {
"pending": "Ve frontě",
"inProgress": "Běží",
"running": "Běží",
"partitioning": "Běží",
"paused": "Pozastaveno",
"completed": "Dokončeno",
"failed": "Neúspěšné",
"faulted": "Neúspěšné",
"skipped": "Zrušeno",
"cancelled": "Zrušeno"
},
"dialog": {
"confirmButtonText": "Potvrdit",
"cancelButtonText": "Zrušit"
}
}
} }
} }
+174 -13
View File
@@ -34,6 +34,8 @@
"browse": "Durchsuchen", "browse": "Durchsuchen",
"increaseValueBy1": "Wert um 1 erhöhen", "increaseValueBy1": "Wert um 1 erhöhen",
"decreaseValueBy1": "Wert um 1 verringern", "decreaseValueBy1": "Wert um 1 verringern",
"on": "Ein",
"off": "Aus",
"preview": "Vorschau" "preview": "Vorschau"
}, },
"splashScreen": { "splashScreen": {
@@ -76,7 +78,7 @@
"description": "Erstellen Sie eine Tabelle und interagieren Sie mit Daten über die Shellschnittstelle von PostgreSQL." "description": "Erstellen Sie eine Tabelle und interagieren Sie mit Daten über die Shellschnittstelle von PostgreSQL."
}, },
"vcoreMongo": { "vcoreMongo": {
"title": "Mongo-Shell", "title": "Mongo Shell",
"description": "Erstellen Sie eine Sammlung und interagieren Sie mit Daten über die Shellschnittstelle von MongoDB." "description": "Erstellen Sie eine Sammlung und interagieren Sie mit Daten über die Shellschnittstelle von MongoDB."
} }
}, },
@@ -469,7 +471,7 @@
"sharded": "Mit Sharding", "sharded": "Mit Sharding",
"addPartitionKey": "Hierarchischen Partitionsschlüssel hinzufügen", "addPartitionKey": "Hierarchischen Partitionsschlüssel hinzufügen",
"hierarchicalPartitionKeyInfo": "Mit diesem Feature können Sie Ihre Daten mit bis zu drei Schlüsselebenen partitionieren, um eine bessere Datenverteilung zu erzielen. Erfordert .NET V3, Java V4 SDK oder JavaScript V3 SDK (Vorschauversion).", "hierarchicalPartitionKeyInfo": "Mit diesem Feature können Sie Ihre Daten mit bis zu drei Schlüsselebenen partitionieren, um eine bessere Datenverteilung zu erzielen. Erfordert .NET V3, Java V4 SDK oder JavaScript V3 SDK (Vorschauversion).",
"provisionDedicatedThroughput": "Dedizierten Durchsatz für {{collectionName}} bereitstellen", "provisionDedicatedThroughput": "Dedizierten Durchsatz für diese {{collectionName}} bereitstellen",
"provisionDedicatedThroughputTooltip": "Sie können optional dedizierten Durchsatz für eine {{collectionName}} in einer Datenbank bereitstellen, für die Durchsatz bereitgestellt wurde. Dieser dedizierte Durchsatz wird nicht für andere {{collectionNamePlural}} in der Datenbank freigegeben und zählt nicht zum Durchsatz, den Sie für die Datenbank bereitgestellt haben. Diese Durchsatzmenge wird zusätzlich zu dem Durchsatz, den Sie auf Datenbankebene bereitgestellt haben, in Rechnung gestellt.", "provisionDedicatedThroughputTooltip": "Sie können optional dedizierten Durchsatz für eine {{collectionName}} in einer Datenbank bereitstellen, für die Durchsatz bereitgestellt wurde. Dieser dedizierte Durchsatz wird nicht für andere {{collectionNamePlural}} in der Datenbank freigegeben und zählt nicht zum Durchsatz, den Sie für die Datenbank bereitgestellt haben. Diese Durchsatzmenge wird zusätzlich zu dem Durchsatz, den Sie auf Datenbankebene bereitgestellt haben, in Rechnung gestellt.",
"uniqueKeysPlaceholderMongo": "Durch Trennzeichen getrennte Pfade, z. B. firstName,address.zipCode", "uniqueKeysPlaceholderMongo": "Durch Trennzeichen getrennte Pfade, z. B. firstName,address.zipCode",
"uniqueKeysPlaceholderSql": "Durch Trennzeichen getrennte Pfade, z. B. /firstName,/address/zipCode", "uniqueKeysPlaceholderSql": "Durch Trennzeichen getrennte Pfade, z. B. /firstName,/address/zipCode",
@@ -735,7 +737,7 @@
"addProperty": "Eigenschaft hinzufügen" "addProperty": "Eigenschaft hinzufügen"
}, },
"addGlobalSecondaryIndex": { "addGlobalSecondaryIndex": {
"globalSecondaryIndexId": "Container-ID des globalen sekundären Indexes", "globalSecondaryIndexId": "Container-ID des globalen sekundären Index",
"globalSecondaryIndexIdPlaceholder": "Beispiel: indexbyEmailId", "globalSecondaryIndexIdPlaceholder": "Beispiel: indexbyEmailId",
"projectionQuery": "Projektionsabfrage", "projectionQuery": "Projektionsabfrage",
"projectionQueryPlaceholder": "SELECT c.email, c.accountId FROM c", "projectionQueryPlaceholder": "SELECT c.email, c.accountId FROM c",
@@ -762,7 +764,7 @@
"computedProperties": "Berechnete Eigenschaften", "computedProperties": "Berechnete Eigenschaften",
"containerPolicies": "Containerrichtlinien", "containerPolicies": "Containerrichtlinien",
"throughputBuckets": "Durchsatzbuckets", "throughputBuckets": "Durchsatzbuckets",
"globalSecondaryIndexPreview": "Globaler sekundärer Index (Vorschau)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "Maskierungsrichtlinie" "maskingPolicyPreview": "Maskierungsrichtlinie"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -795,7 +797,7 @@
"perMonth": "/Mo." "perMonth": "/Mo."
}, },
"throughput": { "throughput": {
"manualToAutoscaleDisclaimer": "Die maximale RU/s-Anzahl für die Autoskalierung wird vom System basierend auf den aktuellen manuellen Durchsatzeinstellungen und dem Speicher Ihrer Ressource festgelegt. Nachdem die Autoskalierung aktiviert wurde, können Sie die maximale RU/s-Anzahl ändern.", "manualToAutoscaleDisclaimer": "Der Ausgangswert für maximale RU/s für die Autoskalierung wird vom System basierend auf den aktuellen Einstellungen für den manuellen Durchsatz und dem Speicher Ihrer Ressource festgelegt. Nachdem die Autoskalierung aktiviert wurde, können Sie den Wert für maximale RU/s ändern.",
"ttlWarningText": "Das System löscht Elemente automatisch auf Grundlage des von Ihnen angegebenen TTL-Werts (in Sekunden), ohne dass eine Löschoperation explizit von einer Clientanwendung angefordert werden muss. Weitere Informationen finden Sie unter", "ttlWarningText": "Das System löscht Elemente automatisch auf Grundlage des von Ihnen angegebenen TTL-Werts (in Sekunden), ohne dass eine Löschoperation explizit von einer Clientanwendung angefordert werden muss. Weitere Informationen finden Sie unter",
"ttlWarningLinkText": "Gültigkeitsdauer (TTL) in Azure Cosmos DB", "ttlWarningLinkText": "Gültigkeitsdauer (TTL) in Azure Cosmos DB",
"unsavedIndexingPolicy": "Indizierungsrichtlinie", "unsavedIndexingPolicy": "Indizierungsrichtlinie",
@@ -807,12 +809,12 @@
"scalingUpDelayMessage": "Das Hochskalieren dauert 46 Stunden, da es die sofortige Unterstützung von Azure Cosmos DB basierend auf Ihrer Anzahl physischer Partitionen übersteigt. Sie können Ihren Durchsatz sofort auf {{instantMaximumThroughput}} erhöhen oder mit dem aktuellen Wert fortfahren und warten, bis das Hochskalieren abgeschlossen ist.", "scalingUpDelayMessage": "Das Hochskalieren dauert 46 Stunden, da es die sofortige Unterstützung von Azure Cosmos DB basierend auf Ihrer Anzahl physischer Partitionen übersteigt. Sie können Ihren Durchsatz sofort auf {{instantMaximumThroughput}} erhöhen oder mit dem aktuellen Wert fortfahren und warten, bis das Hochskalieren abgeschlossen ist.",
"exceedPreAllocatedMessage": "Ihre Anfrage zur Erhöhung des Durchsatzes übersteigt die vorab zugewiesene Kapazität, was zu einer längeren Bearbeitungszeit als erwartet führen kann. Sie können aus drei Optionen wählen, um fortzufahren:", "exceedPreAllocatedMessage": "Ihre Anfrage zur Erhöhung des Durchsatzes übersteigt die vorab zugewiesene Kapazität, was zu einer längeren Bearbeitungszeit als erwartet führen kann. Sie können aus drei Optionen wählen, um fortzufahren:",
"instantScaleOption": "Sie können sofort auf {{instantMaximumThroughput}} RU/s hochskalieren.", "instantScaleOption": "Sie können sofort auf {{instantMaximumThroughput}} RU/s hochskalieren.",
"asyncScaleOption": "Sie können asynchron innerhalb von 46 Stunden auf einen beliebigen Wert unter {{maximumThroughput}} RU/s skalieren.", "asyncScaleOption": "Sie können asynchron innerhalb von 46 Stunden auf einen beliebigen Wert unter {{maximumThroughput}} RU/s hochskalieren.",
"quotaMaxOption": "Ihr aktuelles Kontingent liegt bei {{maximumThroughput}} RU/s. Um dieses Limit zu überschreiten, müssen Sie eine Kontingenterhöhung anfordern und das Azure Cosmos DB-Team wird diese prüfen.", "quotaMaxOption": "Ihr aktuelles Kontingent liegt bei {{maximumThroughput}} RU/s. Um dieses Limit zu überschreiten, müssen Sie eine Kontingenterhöhung anfordern, und das Azure Cosmos DB-Team wird diese prüfen.",
"belowMinimumMessage": "Sie können den Durchsatz nicht unter den aktuellen Mindestwert von {{minimum}} RU/s senken. Weitere Informationen zu diesem Limit finden Sie in unserer Dokumentation zum Serviceangebot.", "belowMinimumMessage": "Sie können den Durchsatz nicht unter den aktuellen Mindestwert von {{minimum}} RU/s senken. Weitere Informationen zu diesem Limit finden Sie in unserer Dokumentation zum Serviceangebot.",
"saveThroughputWarning": "Ihre Rechnung wird sich ändern, wenn Sie Ihre Durchsatzeinstellungen aktualisieren. Bitte überprüfen Sie die untenstehende aktualisierte Kostenschätzung, bevor Sie Ihre Änderungen speichern.", "saveThroughputWarning": "Ihre Rechnung wird sich ändern, wenn Sie Ihre Durchsatzeinstellungen aktualisieren. Bitte überprüfen Sie die untenstehende aktualisierte Kostenschätzung, bevor Sie Ihre Änderungen speichern.",
"currentAutoscaleThroughput": "Aktueller Durchsatz der Autoskalierung:", "currentAutoscaleThroughput": "Aktueller Autoskalierungsdurchsatz:",
"targetAutoscaleThroughput": "Zieldurchsatz für Autoskalierung:", "targetAutoscaleThroughput": "Autoskalierungs-Zieldurchsatz:",
"currentManualThroughput": "Aktueller manueller Durchsatz:", "currentManualThroughput": "Aktueller manueller Durchsatz:",
"targetManualThroughput": "Manueller Zieldurchsatz:", "targetManualThroughput": "Manueller Zieldurchsatz:",
"applyDelayedMessage": "Die Anforderung zur Erhöhung des Durchsatzes wurde erfolgreich übermittelt. Dieser Vorgang dauert 13 Werktage. Die neuesten Statusinformationen finden Sie in den Benachrichtigungen.", "applyDelayedMessage": "Die Anforderung zur Erhöhung des Durchsatzes wurde erfolgreich übermittelt. Dieser Vorgang dauert 13 Werktage. Die neuesten Statusinformationen finden Sie in den Benachrichtigungen.",
@@ -843,7 +845,7 @@
"disclaimerCompoundIndexesLink": " Zusammengesetzte Indizes ", "disclaimerCompoundIndexesLink": " Zusammengesetzte Indizes ",
"disclaimerSuffix": "werden nur zum Sortieren von Abfrageergebnissen verwendet. Wenn Sie einen zusammengesetzten Index hinzufügen müssen, können Sie diesen mit der Mongo Shell erstellen.", "disclaimerSuffix": "werden nur zum Sortieren von Abfrageergebnissen verwendet. Wenn Sie einen zusammengesetzten Index hinzufügen müssen, können Sie diesen mit der Mongo Shell erstellen.",
"compoundNotSupported": "Sammlungen mit zusammengesetzten Indizes werden im Indizierungseditor derzeit noch nicht unterstützt. Um die Indizierungsrichtlinie für diese Sammlung zu ändern, verwenden Sie die Mongo Shell.", "compoundNotSupported": "Sammlungen mit zusammengesetzten Indizes werden im Indizierungseditor derzeit noch nicht unterstützt. Um die Indizierungsrichtlinie für diese Sammlung zu ändern, verwenden Sie die Mongo Shell.",
"aadError": "Um den Indexierungsrichtlinien-Editor zu verwenden, melden Sie sich bei", "aadError": "Um den Indizierungsrichtlinien-Editor zu verwenden, bitte anmelden bei",
"aadErrorLink": "Azure-Portal.", "aadErrorLink": "Azure-Portal.",
"refreshingProgress": "Fortschritt der Indextransformation wird aktualisiert", "refreshingProgress": "Fortschritt der Indextransformation wird aktualisiert",
"canMakeMoreChangesZero": "Sobald die aktuelle Indextransformation abgeschlossen ist, können Sie weitere Änderungen an der Indizierung vornehmen. ", "canMakeMoreChangesZero": "Sobald die aktuelle Indextransformation abgeschlossen ist, können Sie weitere Änderungen an der Indizierung vornehmen. ",
@@ -925,10 +927,10 @@
}, },
"globalSecondaryIndex": { "globalSecondaryIndex": {
"indexesDefined": "Für diesen Container sind die folgenden Indizes definiert.", "indexesDefined": "Für diesen Container sind die folgenden Indizes definiert.",
"learnMoreSuffix": "über die Definition globaler Sekundärindizes und deren Verwendung.", "learnMoreSuffix": "über die Definition globaler sekundärer Indizes und ihre Verwendung.",
"jsonAriaLabel": "JSON für globalen sekundären Index", "jsonAriaLabel": "JSON für globalen sekundären Index",
"addIndex": "Index hinzufügen", "addIndex": "Index hinzufügen",
"settingsTitle": "Globale Einstellungen für sekundären Index", "settingsTitle": "Einstellungen für globalen sekundären Index",
"sourceContainer": "Quellcontainer", "sourceContainer": "Quellcontainer",
"indexDefinition": "Definition des globalen sekundären Index" "indexDefinition": "Definition des globalen sekundären Index"
}, },
@@ -947,7 +949,7 @@
"instant": "Sofort", "instant": "Sofort",
"fourToSixHrs": "46 Stunden", "fourToSixHrs": "46 Stunden",
"autoscaleDescription": "Basierend auf der Nutzung wird Ihr {{resourceType}}-Durchsatz von", "autoscaleDescription": "Basierend auf der Nutzung wird Ihr {{resourceType}}-Durchsatz von",
"freeTierWarning": "Die Abrechnung erfolgt, wenn Sie mehr als {{ru}} RU/s manuellen Durchsatz bereitstellen oder wenn die Ressource mit Autoscale über {{ru}} RU/s skaliert.", "freeTierWarning": "Eine Abrechnung findet statt, wenn Sie einen manuellen Durchsatz von mehr als {{ru}} RU/s bereitstellen oder wenn die Ressource mit Autoskalierung auf über {{ru}} RU/s skaliert wird.",
"capacityCalculator": "Schätzen Sie Ihren erforderlichen RU/s mit", "capacityCalculator": "Schätzen Sie Ihren erforderlichen RU/s mit",
"capacityCalculatorLink": " Kapazitätsrechner", "capacityCalculatorLink": " Kapazitätsrechner",
"fixedStorageNote": "Wenn Sie eine Sammlung mit fester Speicherkapazität verwenden, können Sie bis zu 10.000 RU/s festlegen.", "fixedStorageNote": "Wenn Sie eine Sammlung mit fester Speicherkapazität verwenden, können Sie bis zu 10.000 RU/s festlegen.",
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "Die Quantisierungsbytegröße muss größer als 0 und kleiner oder gleich 512 sein.", "quantizationByteSizeRangeError": "Die Quantisierungsbytegröße muss größer als 0 und kleiner oder gleich 512 sein.",
"indexingSearchListSizeRangeError": "Die Größe der Indizierungssuchliste muss größer oder gleich 25 und kleiner oder gleich 500 sein." "indexingSearchListSizeRangeError": "Die Größe der Indizierungssuchliste muss größer oder gleich 25 und kleiner oder gleich 500 sein."
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Feedback",
"feedbackButtonAriaLabel": "Feedback zu Kopieraufträgen geben",
"refreshButtonAriaLabel": "Kopieraufträge aktualisieren",
"createCopyJobButtonLabel": "Kopierauftrag erstellen",
"createCopyJobButtonAriaLabel": "Neuen Containerkopierauftrag erstellen"
},
"noCopyJobs": {
"title": "Keine Kopieraufträge zum Anzeigen vorhanden",
"createCopyJobButtonText": "Containerkopierauftrag erstellen"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Auftragsdetails",
"errorTitle": "Fehlerdetails",
"selectedContainers": "Ausgewählte Container"
},
"createCopyJob": {
"panelTitle": "Kopierauftrag erstellen"
},
"selectAccount": {
"description": "Wählen Sie ein Zielkonto aus, in das kopiert werden soll.",
"subscriptionDropdownLabel": "Abonnement",
"subscriptionDropdownPlaceholder": "Abonnement auswählen",
"accountDropdownLabel": "Konto",
"accountDropdownPlaceholder": "Konto auswählen"
},
"migrationType": {
"offline": {
"title": "Offlinemodus",
"description": "Mit Offline-Containerkopieraufträgen können Sie Daten aus einem Quellcontainer in einen Cosmos DB-Zielcontainer für unterstützte APIs kopieren. Um die Datenintegrität zwischen Quelle und Ziel sicherzustellen, wird empfohlen, vor dem Erstellen des Kopierauftrags Updates für den Quellcontainer zu beenden. Weitere Informationen zu [Offlinekopieraufträgen](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql)."
},
"online": {
"title": "Onlinemodus",
"description": "Mit Onlinecontainerkopieraufträgen können Sie Daten mithilfe des Änderungsfeeds [Alle Versionen und Löschvorgänge](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) aus einem Quellcontainer in einen Cosmos DB NoSQL-API-Zielcontainer kopieren. Dadurch können Updates in der Quelle fortgesetzt werden, während Daten kopiert werden. Am Ende ist eine kurze Downtime erforderlich, um sicher über Clientanwendungen zum Zielcontainer zu wechseln. Weitere Informationen zu [Onlinekopieraufträgen](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
"selectContainers": {
"description": "Wählen Sie einen Quellcontainer und einen Zielcontainer aus, in den kopiert werden soll.",
"sourceContainerSubHeading": "Quellcontainer",
"targetContainerSubHeading": "Zielcontainer",
"databaseDropdownLabel": "Datenbank",
"databaseDropdownPlaceholder": "Datenbank auswählen",
"containerDropdownLabel": "Container",
"containerDropdownPlaceholder": "Container auswählen",
"createNewContainerSubHeading": "Konfigurieren Sie die Eigenschaften für den neuen Container im Zielkonto „{{accountName}}“.",
"createNewContainerSubHeadingDefault": "Konfigurieren Sie die Eigenschaften für den neuen Container.",
"createContainerButtonLabel": "Neuen Container erstellen",
"createContainerHeading": "Neuen Container erstellen"
},
"preview": {
"jobNameLabel": "Auftragsname",
"subscriptionLabel": "Zielabonnement",
"accountLabel": "Zielkonto",
"sourceDatabaseLabel": "Quelldatenbank",
"sourceContainerLabel": "Quellcontainer",
"targetDatabaseLabel": "Zieldatenbank",
"targetContainerLabel": "Zielcontainer"
},
"assignPermissions": {
"crossAccountDescription": "Um Daten aus der Quelle in den Zielcontainer zu kopieren, stellen Sie sicher, dass die verwaltete Identität des Quellkontos Lese-/Schreibzugriff auf das Zielkonto hat, indem Sie die folgenden Schritte ausführen.",
"intraAccountOnlineDescription": "Führen Sie die folgenden Schritte aus, um das Onlinekopieren für Ihr Konto „{{accountName}}“ zu aktivieren.",
"crossAccountConfiguration": {
"title": "Kontoübergreifende Containerkopie",
"description": "Befolgen Sie die Anweisungen unten, um die erforderlichen Berechtigungen zum Kopieren von Daten von „{{sourceAccount}}“ nach „{{destinationAccount}}“ zu erteilen."
},
"onlineConfiguration": {
"title": "Onlinecontainerkopie",
"description": "Befolgen Sie die nachstehenden Anweisungen, um das Onlinekopieren für Ihr Konto „{{accountName}}“ zu aktivieren."
}
},
"popoverOverlaySpinnerLabel": "Bitte warten Sie, während wir Ihre Anforderung verarbeiten ...",
"addManagedIdentity": {
"title": "Systemseitig zugewiesene verwaltete Identität aktiviert.",
"description": "Eine systemseitig zugewiesene verwaltete Identität ist auf eine pro Ressource beschränkt und an den Lebenszyklus dieser Ressource gebunden. Nach ihrer Aktivierung können Sie der verwalteten Identität mithilfe der rollenbasierten Azure-Zugriffssteuerung(Azure Role Based Access Control, Azure RBAC) Berechtigungen erteilen. Die verwaltete Identität über Microsoft Entra ID authentifiziert wird, sodass Sie keine Anmeldeinformationen im Code speichern müssen.",
"descriptionHrefText": "Weitere Informationen zu verwalteten Identitäten.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "Systemseitig zugewiesene verwaltete Identität",
"tooltipContent": "Weitere Informationen zu",
"tooltipHrefText": "Verwaltete Identitäten.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "Sie können eine vorhandene benutzerseitig zugewiesene Identität auswählen oder eine neue erstellen.",
"userAssignedIdentityLabel": "Sie können auch eine benutzerseitig zugewiesene verwaltete Identität auswählen.",
"createUserAssignedIdentityLink": "Benutzerseitig zugewiesene verwaltete Identität erstellen",
"enablementTitle": "Systemseitig zugewiesene verwaltete Identität aktivieren",
"enablementDescription": "Aktivieren Sie die systemseitig zugewiesene verwaltete Identität für {{accountName}}. Klicken Sie zur Bestätigung auf die Schaltfläche „Ja“."
},
"defaultManagedIdentity": {
"title": "Systemseitig zugewiesene verwaltete Identität wurde als Standard festgelegt.",
"description": "Legen Sie die systemseitig zugewiesene verwaltete Identität als Standard für „{{accountName}}“ fest, indem Sie sie aktivieren.",
"tooltipContent": "Weitere Informationen zu",
"tooltipHrefText": "Verwaltete Standardidentitäten.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "Systemseitig zugewiesene verwaltete Identität als Standard festlegen",
"popoverDescription": "Weisen Sie die systemseitig zugewiesene verwaltete Identität als Standard für „{{accountName}}“ zu. Klicken Sie zur Bestätigung auf die Schaltfläche „Ja“. "
},
"readWritePermissionAssigned": {
"title": "Der Standardidentität wurden Lese-/Schreibberechtigungen zugewiesen.",
"description": "Um das Kopieren von Daten aus der Quelle in den Zielcontainer zuzulassen, gewähren Sie der Standardidentität des Quellkontos Lese- und Schreibzugriff auf das Zielkonto.",
"tooltipContent": "Weitere Informationen zu",
"tooltipHrefText": "Lese-/Schreibberechtigungen.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Weisen Sie der Standardidentität Lese-/Schreibberechtigungen zu.",
"popoverDescription": "Weisen Sie der Standardidentität des Quellkontos Lese-/Schreibberechtigungen für das Zielkonto zu. Klicken Sie zur Bestätigung auf die Schaltfläche „Ja“."
},
"pointInTimeRestore": {
"title": "Point-in-Time-Wiederherstellung aktiviert",
"description": "Um Aufträge zum Kopieren von Onlinecontainern zu vereinfachen, aktualisieren Sie Ihre Sicherungsrichtlinie „{{accessName}}“ von regelmäßigen auf fortlaufende Sicherungen. Für diese Funktionalität ist die Aktivierung der fortlaufenden Sicherung erforderlich.",
"tooltipContent": "Weitere Informationen zu",
"tooltipHrefText": "Fortlaufende Sicherung",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "Zeitpunktwiederherstellung aktivieren"
},
"onlineCopyEnabled": {
"title": "Onlinekopie aktiviert",
"description": "Aktivieren Sie das Kopieren von Onlinecontainern, indem Sie auf die Schaltfläche unten in Ihrem Konto „{{accountName}}“ klicken.",
"hrefText": "Weitere Informationen zu Onlinekopieraufträgen",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "Onlinekopie aktivieren",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Änderungsfeedmodus „Alle Versionen und Löschvorgänge“ (Vorschau) wird validiert ...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Änderungsfeedmodus „Alle Versionen und Löschvorgänge“ (Vorschau) wird aktiviert",
"enablingOnlineCopySpinnerLabel": "Onlinekopie wird für Ihr Konto „{{accountName}}“ aktiviert ..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "Datum und Uhrzeit",
"name": "Auftragsname",
"status": "Status",
"completionPercentage": "Abschluss %",
"duration": "Dauer",
"error": "Fehlermeldung",
"mode": "Modus",
"actions": "Aktionen"
},
"actions": {
"pause": "Anhalten",
"resume": "Fortsetzen",
"complete": "Abschließen",
"viewDetails": "Details anzeigen"
},
"status": {
"pending": "In Warteschlange",
"inProgress": "Wird ausgeführt",
"running": "Wird ausgeführt",
"partitioning": "Wird ausgeführt",
"paused": "Angehalten",
"completed": "Abgeschlossen",
"failed": "Fehler",
"faulted": "Fehler",
"skipped": "Abgebrochen",
"cancelled": "Abgebrochen"
},
"dialog": {
"confirmButtonText": "Bestätigen",
"cancelButtonText": "Abbrechen"
}
}
} }
} }
+179 -3
View File
@@ -34,6 +34,8 @@
"browse": "Browse", "browse": "Browse",
"increaseValueBy1": "Increase value by 1", "increaseValueBy1": "Increase value by 1",
"decreaseValueBy1": "Decrease value by 1", "decreaseValueBy1": "Decrease value by 1",
"on": "On",
"off": "Off",
"preview": "Preview" "preview": "Preview"
}, },
"splashScreen": { "splashScreen": {
@@ -758,11 +760,10 @@
"settings": "Settings", "settings": "Settings",
"indexingPolicy": "Indexing Policy", "indexingPolicy": "Indexing Policy",
"partitionKeys": "Partition Keys", "partitionKeys": "Partition Keys",
"partitionKeysPreview": "Partition Keys (preview)",
"computedProperties": "Computed Properties", "computedProperties": "Computed Properties",
"containerPolicies": "Container Policies", "containerPolicies": "Container Policies",
"throughputBuckets": "Throughput Buckets", "throughputBuckets": "Throughput Buckets",
"globalSecondaryIndexPreview": "Global Secondary Index (Preview)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "Masking Policy" "maskingPolicyPreview": "Masking Policy"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -893,6 +894,10 @@
}, },
"partitionKeyEditor": { "partitionKeyEditor": {
"changePartitionKey": "Change {{partitionKeyName}}", "changePartitionKey": "Change {{partitionKeyName}}",
"confirmCancel1": "You are about to cancel the following copy job.",
"confirmCancel2": "Cancelling will stop the job immediately.",
"confirmComplete1": "You are about to complete the following copy job.",
"confrimComplete2": "Once completed, continuous data copy will stop after any pending documents are processed. To maintain data integrity, we recommend stopping updates to the source container before completing the job.",
"currentPartitionKey": "Current {{partitionKeyName}}", "currentPartitionKey": "Current {{partitionKeyName}}",
"partitioning": "Partitioning", "partitioning": "Partitioning",
"hierarchical": "Hierarchical", "hierarchical": "Hierarchical",
@@ -992,5 +997,176 @@
"quantizationByteSizeRangeError": "Quantization byte size must be greater than 0 and less than or equal to 512", "quantizationByteSizeRangeError": "Quantization byte size must be greater than 0 and less than or equal to 512",
"indexingSearchListSizeRangeError": "Indexing search list size must be greater than or equal to 25 and less than or equal to 500" "indexingSearchListSizeRangeError": "Indexing search list size must be greater than or equal to 25 and less than or equal to 500"
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Feedback",
"feedbackButtonAriaLabel": "Provide feedback on copy jobs",
"refreshButtonAriaLabel": "Refresh copy jobs",
"createCopyJobButtonLabel": "Create Copy Job",
"createCopyJobButtonAriaLabel": "Create a new container copy job"
},
"noCopyJobs": {
"title": "No copy jobs to show",
"createCopyJobButtonText": "Create a container copy job"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Job Details",
"errorTitle": "Error Details",
"selectedContainers": "Selected Containers"
},
"createCopyJob": {
"panelTitle": "Create copy job"
},
"selectAccount": {
"description": "Please select a destination account to copy to.",
"subscriptionDropdownLabel": "Subscription",
"subscriptionDropdownPlaceholder": "Select a subscription",
"accountDropdownLabel": "Account",
"accountDropdownPlaceholder": "Select an account"
},
"migrationType": {
"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)."
}
},
"migrationTypeTitle": "Migration type",
"selectContainers": {
"description": "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": "Configure the properties for the new container on destination account \"{{accountName}}\".",
"createNewContainerSubHeadingDefault": "Configure the properties for the new container.",
"createContainerButtonLabel": "Create a new container",
"createContainerHeading": "Create new container"
},
"preview": {
"jobNameLabel": "Job name",
"subscriptionLabel": "Destination subscription",
"accountLabel": "Destination account",
"sourceDatabaseLabel": "Source database",
"sourceContainerLabel": "Source container",
"targetDatabaseLabel": "Destination database",
"targetContainerLabel": "Destination container"
},
"assignPermissions": {
"crossAccountDescription": "To copy data from the source to the destination container, ensure that the managed identity of the source account has read-write access to the destination account by completing the following steps.",
"intraAccountOnlineDescription": "Follow the steps below to enable online copy on your \"{{accountName}}\" account.",
"crossAccountConfiguration": {
"title": "Cross-account container copy",
"description": "Please follow the instruction below to grant requisite permissions to copy data from \"{{sourceAccount}}\" to \"{{destinationAccount}}\"."
},
"onlineConfiguration": {
"title": "Online container copy",
"description": "Please follow the instructions below to enable online copy on your \"{{accountName}}\" account."
}
},
"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",
"tooltipContent": "Learn more about",
"tooltipHrefText": "Managed Identities.",
"tooltipHref": "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": "Enable system-assigned managed identity on the {{accountName}}. To confirm, click the \"Yes\" button."
},
"defaultManagedIdentity": {
"title": "System-assigned managed identity set as default.",
"description": "Set the system-assigned managed identity as default for \"{{accountName}}\" by switching it on.",
"tooltipContent": "Learn more about",
"tooltipHrefText": "Default Managed Identities.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "System assigned managed identity set as default",
"popoverDescription": "Assign the system-assigned managed identity as the default for \"{{accountName}}\". To confirm, click the \"Yes\" button. "
},
"readWritePermissionAssigned": {
"title": "Read-write permissions assigned to the default identity.",
"description": "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.",
"tooltipContent": "Learn more about",
"tooltipHrefText": "Read-write permissions.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Assign read-write permissions to default identity.",
"popoverDescription": "Assign read-write permissions on the destination account to the default identity of the source account. To confirm, click the \"Yes\" button."
},
"pointInTimeRestore": {
"title": "Point In Time Restore enabled",
"description": "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.",
"tooltipContent": "Learn more about",
"tooltipHrefText": "Continuous Backup",
"tooltipHref": "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": "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": "Enabling online copy on your \"{{accountName}}\" account ...",
"onlineMigrationPrerequisitesMessage": "Online migration prerequisites must be enabled before proceeding."
},
"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",
"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": {
"confirmButtonText": "Confirm",
"cancelButtonText": "Cancel"
}
},
"dataTransfers": {
"polling": {
"cancelConsoleMessage": "Data transfer job \"{{jobName}}\" cancelled",
"completedConsoleMessage": "Data transfer job \"{{jobName}}\" completed",
"defaultErrorMessage": "Operation could not be completed",
"errorConsoleMessage": "Data transfer job \"{{jobName}}\" failed: {{errorMessage}}",
"pauseConsoleMessage": "Data transfer job \"{{jobName}}\" paused",
"retryConsoleMessage": "Data transfer job \"{{jobName}}\" in progress, total count: {{totalCount}}, processed count: {{processedCount}}"
}
}
} }
} }
+162 -1
View File
@@ -34,6 +34,8 @@
"browse": "Examinar", "browse": "Examinar",
"increaseValueBy1": "Aumentar valor en 1", "increaseValueBy1": "Aumentar valor en 1",
"decreaseValueBy1": "Disminuir valor en 1", "decreaseValueBy1": "Disminuir valor en 1",
"on": "Activado",
"off": "Desactivado",
"preview": "Versión preliminar" "preview": "Versión preliminar"
}, },
"splashScreen": { "splashScreen": {
@@ -762,7 +764,7 @@
"computedProperties": "Propiedades calculadas", "computedProperties": "Propiedades calculadas",
"containerPolicies": "Directivas de contenedor", "containerPolicies": "Directivas de contenedor",
"throughputBuckets": "Depósitos de rendimiento", "throughputBuckets": "Depósitos de rendimiento",
"globalSecondaryIndexPreview": "Índice secundario global (versión preliminar)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "Directiva de enmascaramiento" "maskingPolicyPreview": "Directiva de enmascaramiento"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "El tamaño de bytes de cuantificación debe ser mayor que 0 y menor o igual que 512", "quantizationByteSizeRangeError": "El tamaño de bytes de cuantificación debe ser mayor que 0 y menor o igual que 512",
"indexingSearchListSizeRangeError": "El tamaño de la lista de búsqueda de indexación debe ser mayor o igual que 25 y menor o igual que 500" "indexingSearchListSizeRangeError": "El tamaño de la lista de búsqueda de indexación debe ser mayor o igual que 25 y menor o igual que 500"
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Comentarios",
"feedbackButtonAriaLabel": "Proporcionar comentarios sobre los trabajos de copia",
"refreshButtonAriaLabel": "Actualizar trabajos de copia",
"createCopyJobButtonLabel": "Crear trabajo de copia",
"createCopyJobButtonAriaLabel": "Creación de un nuevo trabajo de copia de contenedor"
},
"noCopyJobs": {
"title": "No hay trabajos de copia que mostrar",
"createCopyJobButtonText": "Creación de un trabajo de copia de contenedor"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Detalles del trabajo",
"errorTitle": "Detalles del error",
"selectedContainers": "Contenedores seleccionados"
},
"createCopyJob": {
"panelTitle": "Crear trabajo de copia"
},
"selectAccount": {
"description": "Seleccione una cuenta de destino en la que copiar.",
"subscriptionDropdownLabel": "Suscripción",
"subscriptionDropdownPlaceholder": "Seleccionar una suscripción",
"accountDropdownLabel": "Cuenta",
"accountDropdownPlaceholder": "Seleccione una cuenta"
},
"migrationType": {
"offline": {
"title": "Modo sin conexión",
"description": "Los trabajos de copia de contenedor sin conexión permiten copiar datos de un contenedor de origen a un contenedor de Cosmos DB de destino para las API admitidas. Para garantizar la integridad de los datos entre el origen y el destino, se recomienda detener las actualizaciones en el contenedor de origen antes de crear el trabajo de copia. Obtenga más información sobre [trabajos de copia sin conexión](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql)."
},
"online": {
"title": "Modo en línea",
"description": "Los trabajos de copia de contenedores en línea permiten copiar datos de un contenedor de origen a un contenedor de destino de la API NoSQL de Cosmos DB mediante la fuente de cambios [Todas las versiones y eliminación](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Esto permite que las actualizaciones continúen en el origen mientras se copian los datos. Se requiere un breve tiempo de inactividad al final para cambiar de forma segura las aplicaciones cliente al contenedor de destino. Obtenga más información sobre [trabajos de copia online](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
"selectContainers": {
"description": "Seleccione un contenedor de origen y un contenedor de destino en el que copiar.",
"sourceContainerSubHeading": "Contenedor de origen",
"targetContainerSubHeading": "Contenedor de destino",
"databaseDropdownLabel": "Base de datos",
"databaseDropdownPlaceholder": "Seleccionar una base de datos",
"containerDropdownLabel": "Contenedor",
"containerDropdownPlaceholder": "Seleccione un contenedor",
"createNewContainerSubHeading": "Configure las propiedades del nuevo contenedor en la cuenta de destino \"{{accountName}}\".",
"createNewContainerSubHeadingDefault": "Configure las propiedades del nuevo contenedor.",
"createContainerButtonLabel": "Crear contenedor nuevo",
"createContainerHeading": "Crear nuevo contenedor"
},
"preview": {
"jobNameLabel": "Nombre del trabajo",
"subscriptionLabel": "Suscripción de destino",
"accountLabel": "Cuenta de destino",
"sourceDatabaseLabel": "Base de datos de origen",
"sourceContainerLabel": "Contenedor de origen",
"targetDatabaseLabel": "Base de datos de destino",
"targetContainerLabel": "Contenedor de destino"
},
"assignPermissions": {
"crossAccountDescription": "Para copiar datos desde el contenedor de origen al de destino, asegúrese de que la identidad administrada de la cuenta de origen tiene acceso de lectura y escritura a la cuenta de destino mediante los pasos siguientes.",
"intraAccountOnlineDescription": "Siga los pasos que se indican a continuación para habilitar la copia en línea en su cuenta \"{{accountName}}\".",
"crossAccountConfiguration": {
"title": "Copia de contenedor entre cuentas",
"description": "Siga las instrucciones siguientes para conceder los permisos necesarios para copiar datos de \"{{sourceAccount}}\" a \"{{destinationAccount}}\"."
},
"onlineConfiguration": {
"title": "Copia de contenedor en línea",
"description": "Siga las instrucciones siguientes para habilitar la copia en línea en su cuenta \"{{accountName}}\"."
}
},
"popoverOverlaySpinnerLabel": "Espere mientras procesamos su solicitud...",
"addManagedIdentity": {
"title": "Identidad administrada asignada por el sistema habilitada.",
"description": "Una identidad administrada asignada por el sistema está restringida a una por recurso y está vinculada al ciclo de vida de este recurso. Una vez habilitado, puede conceder permisos a la identidad administrada mediante el control de acceso basado en rol de Azure (RBAC de Azure). La identidad administrada se autentica con Microsoft Entra ID, de modo que no tiene que almacenar credenciales en el código.",
"descriptionHrefText": "Obtenga más información sobre las identidades administradas.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "Identidad administrada asignada por el sistema",
"tooltipContent": "Más información sobre",
"tooltipHrefText": "Identidades administradas.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "Puede seleccionar una identidad asignada por el usuario existente o crear una nueva.",
"userAssignedIdentityLabel": "También puede seleccionar una identidad administrada asignada por el usuario.",
"createUserAssignedIdentityLink": "Crear identidad administrada asignada por el usuario",
"enablementTitle": "Habilitar identidad administrada asignada por el sistema",
"enablementDescription": "Habilite la identidad administrada asignada por el sistema en el {{accountName}}. Para confirmar, haga clic en el botón \"Sí\"."
},
"defaultManagedIdentity": {
"title": "Identidad administrada asignada por el sistema establecida como predeterminada.",
"description": "Establezca la identidad administrada asignada por el sistema como predeterminada para \"{{accountName}}\" cambiándola a activada.",
"tooltipContent": "Más información sobre",
"tooltipHrefText": "Identidades administradas predeterminadas.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "Identidad administrada asignada por el sistema establecida como predeterminada",
"popoverDescription": "Asigne la identidad administrada asignada por el sistema como valor predeterminado para \"{{accountName}}\". Para confirmar, haga clic en el botón \"Sí\". "
},
"readWritePermissionAssigned": {
"title": "Permisos de lectura y escritura asignados a la identidad predeterminada.",
"description": "Para permitir la copia de datos desde el origen al contenedor de destino, proporcione acceso de lectura y escritura en la cuenta de destino a la identidad predeterminada de la cuenta de origen.",
"tooltipContent": "Más información sobre",
"tooltipHrefText": "Permisos de lectura y escritura.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Asigne permisos de lectura y escritura a la identidad predeterminada.",
"popoverDescription": "Asigne permisos de lectura y escritura en la cuenta de destino a la identidad predeterminada de la cuenta de origen. Para confirmar, haga clic en el botón \"Sí\"."
},
"pointInTimeRestore": {
"title": "Restauración a un momento dado habilitada",
"description": "Para facilitar los trabajos de copia de contenedor en línea, actualice la directiva de periódica \"{{accessName}}\" a copia de seguridad continua. Se requiere habilitar la copia de seguridad continua para esta funcionalidad.",
"tooltipContent": "Más información sobre",
"tooltipHrefText": "Copia de seguridad continua",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "Habilitar restauración a un momento dado"
},
"onlineCopyEnabled": {
"title": "Copia en línea habilitada",
"description": "Habilite la copia de contenedor en línea haciendo clic en el botón siguiente en su cuenta \"{{accountName}}\".",
"hrefText": "Más información sobre los trabajos de copia en línea",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "Habilitar copia en línea",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Validando todas las versiones y elimina el modo de fuente de cambios (versión preliminar)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Habilitando todas las versiones y elimina el modo de fuente de cambios (versión preliminar)...",
"enablingOnlineCopySpinnerLabel": "Habilitando la copia en línea en su cuenta \"{{accountName}}\"..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "Fecha y hora",
"name": "Nombre del trabajo",
"status": "Estado",
"completionPercentage": "Porcentaje de finalización",
"duration": "Duración",
"error": "Mensaje de error",
"mode": "Modo",
"actions": "Acciones"
},
"actions": {
"pause": "Pausar",
"resume": "Reanudar",
"complete": "Completado",
"viewDetails": "Ver detalles"
},
"status": {
"pending": "En cola",
"inProgress": "En ejecución",
"running": "En ejecución",
"partitioning": "En ejecución",
"paused": "En pausa",
"completed": "Completado",
"failed": "Erróneo",
"faulted": "Erróneo",
"skipped": "Cancelado",
"cancelled": "Cancelado"
},
"dialog": {
"confirmButtonText": "Confirmar",
"cancelButtonText": "Cancelar"
}
}
} }
} }
+175 -14
View File
@@ -34,6 +34,8 @@
"browse": "Parcourir", "browse": "Parcourir",
"increaseValueBy1": "Augmenter la valeur de 1", "increaseValueBy1": "Augmenter la valeur de 1",
"decreaseValueBy1": "Diminuer la valeur de 1", "decreaseValueBy1": "Diminuer la valeur de 1",
"on": "Activé",
"off": "Désactivé",
"preview": "Aperçu" "preview": "Aperçu"
}, },
"splashScreen": { "splashScreen": {
@@ -76,7 +78,7 @@
"description": "Créer un tableau et interagir avec les données à laide de linterface dinterpréteur PostgreSQL" "description": "Créer un tableau et interagir avec les données à laide de linterface dinterpréteur PostgreSQL"
}, },
"vcoreMongo": { "vcoreMongo": {
"title": "Interpréteur de commandes Mongo", "title": "Mongo Shell",
"description": "Créer une collection et interagir avec les données à laide de linterface dinterpréteur MongoDB" "description": "Créer une collection et interagir avec les données à laide de linterface dinterpréteur MongoDB"
} }
}, },
@@ -303,7 +305,7 @@
"deleteContainer": "Supprimer {{containerName}}", "deleteContainer": "Supprimer {{containerName}}",
"newSqlQuery": "Nouvelle requête SQL", "newSqlQuery": "Nouvelle requête SQL",
"newQuery": "Nouvelle requête", "newQuery": "Nouvelle requête",
"openMongoShell": "Ouvrir linterpréteur de commandes Mongo", "openMongoShell": "Ouvrir Mongo Shell",
"newShell": "Nouvel interpréteur de commandes", "newShell": "Nouvel interpréteur de commandes",
"openCassandraShell": "Ouvrir linterpréteur de commandes Cassandra", "openCassandraShell": "Ouvrir linterpréteur de commandes Cassandra",
"newStoredProcedure": "Nouvelle procédure stockée", "newStoredProcedure": "Nouvelle procédure stockée",
@@ -414,7 +416,7 @@
"refreshGridFailed": "Nous navons pas pu actualiser la grille des documents" "refreshGridFailed": "Nous navons pas pu actualiser la grille des documents"
}, },
"mongoShell": { "mongoShell": {
"title": "Interpréteur de commandes Mongo" "title": "Mongo Shell"
} }
}, },
"panes": { "panes": {
@@ -488,7 +490,7 @@
"legacySdkInfo": "Pour garantir la compatibilité avec les anciens Kits de développement logiciels (SDK), le conteneur créé utilise un schéma de partitionnement hérité qui prend en charge des valeurs de clés de partition dune taille maximale de 101 octets. Si cette option est activée, vous ne pouvez pas utiliser de clés de partition hiérarchiques.", "legacySdkInfo": "Pour garantir la compatibilité avec les anciens Kits de développement logiciels (SDK), le conteneur créé utilise un schéma de partitionnement hérité qui prend en charge des valeurs de clés de partition dune taille maximale de 101 octets. Si cette option est activée, vous ne pouvez pas utiliser de clés de partition hiérarchiques.",
"indexingOnInfo": "Toutes les propriétés de vos documents seront indexées par défaut pour permettre des requêtes flexibles et efficaces.", "indexingOnInfo": "Toutes les propriétés de vos documents seront indexées par défaut pour permettre des requêtes flexibles et efficaces.",
"indexingOffInfo": "Lindexation sera désactivée. Nous recommandons cette option si vous navez pas besoin dexécuter des requêtes ou si vous neffectuez que des opérations clé-valeur.", "indexingOffInfo": "Lindexation sera désactivée. Nous recommandons cette option si vous navez pas besoin dexécuter des requêtes ou si vous neffectuez que des opérations clé-valeur.",
"indexingOffWarning": "En créant ce conteneur avec lindexation désactivée, vous ne pouvez pas modifier la stratégie dindexation. Les modifications dindexation ne sont autorisées que sur un conteneur disposant dune stratégie dindexation.", "indexingOffWarning": "En créant ce conteneur avec lindexation désactivée, vous ne pouvez pas modifier la politique dindexation. Les modifications dindexation ne sont autorisées que sur un conteneur disposant dune politique dindexation.",
"acknowledgeSpendErrorMonthly": "Prenez en compte lestimation des dépenses mensuelles.", "acknowledgeSpendErrorMonthly": "Prenez en compte lestimation des dépenses mensuelles.",
"acknowledgeSpendErrorDaily": "Prenez en compte lestimation des dépenses quotidiennes.", "acknowledgeSpendErrorDaily": "Prenez en compte lestimation des dépenses quotidiennes.",
"unshardedMaxRuError": "Les collections non partitionnées prennent en charge un maximum de 10 000 RU", "unshardedMaxRuError": "Les collections non partitionnées prennent en charge un maximum de 10 000 RU",
@@ -756,13 +758,13 @@
"scale": "Mise à l’échelle", "scale": "Mise à l’échelle",
"conflictResolution": "Résolution des conflits", "conflictResolution": "Résolution des conflits",
"settings": "Paramètres", "settings": "Paramètres",
"indexingPolicy": "Stratégie d'indexation", "indexingPolicy": "Politique d'indexation",
"partitionKeys": "Clés de partition", "partitionKeys": "Clés de partition",
"partitionKeysPreview": "Clés de partition (aperçu)", "partitionKeysPreview": "Clés de partition (aperçu)",
"computedProperties": "Propriétés calculées", "computedProperties": "Propriétés calculées",
"containerPolicies": "Stratégies relatives aux conteneurs", "containerPolicies": "Stratégies relatives aux conteneurs",
"throughputBuckets": "Bacs de débit", "throughputBuckets": "Bacs de débit",
"globalSecondaryIndexPreview": "Indice mondial du secteur secondaire (Aperçu)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "Stratégie de masquage" "maskingPolicyPreview": "Stratégie de masquage"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -798,7 +800,7 @@
"manualToAutoscaleDisclaimer": "Le débit maximal initial de RU/s de mise à l'échelle automatique sera déterminé par le système, en fonction des paramètres de débit manuels actuels et du stockage de votre ressource. Une fois la mise à l'échelle automatique activée, vous pouvez modifier le nombre maximal d'unités de requête par seconde (RU/s).", "manualToAutoscaleDisclaimer": "Le débit maximal initial de RU/s de mise à l'échelle automatique sera déterminé par le système, en fonction des paramètres de débit manuels actuels et du stockage de votre ressource. Une fois la mise à l'échelle automatique activée, vous pouvez modifier le nombre maximal d'unités de requête par seconde (RU/s).",
"ttlWarningText": "Le système supprimera automatiquement les éléments en fonction de la valeur TTL (en secondes) que vous fournissez, sans qu'une opération de suppression explicite soit nécessaire de la part d'une application cliente. Pour plus d'informations, consultez,", "ttlWarningText": "Le système supprimera automatiquement les éléments en fonction de la valeur TTL (en secondes) que vous fournissez, sans qu'une opération de suppression explicite soit nécessaire de la part d'une application cliente. Pour plus d'informations, consultez,",
"ttlWarningLinkText": "Durée de vie (TTL) dans Azure Cosmos DB", "ttlWarningLinkText": "Durée de vie (TTL) dans Azure Cosmos DB",
"unsavedIndexingPolicy": "stratégie d'indexation", "unsavedIndexingPolicy": "politique d'indexation",
"unsavedDataMaskingPolicy": "Stratégie de masquage des données", "unsavedDataMaskingPolicy": "Stratégie de masquage des données",
"unsavedComputedProperties": "propriétés calculées", "unsavedComputedProperties": "propriétés calculées",
"unsavedEditorWarningPrefix": "Vous n'avez pas enregistré les dernières modifications apportées à votre", "unsavedEditorWarningPrefix": "Vous n'avez pas enregistré les dernières modifications apportées à votre",
@@ -841,9 +843,9 @@
"mongoIndexing": { "mongoIndexing": {
"disclaimer": "Pour les requêtes qui filtrent sur plusieurs propriétés, créez plusieurs index à champ unique au lieu d'un index composé.", "disclaimer": "Pour les requêtes qui filtrent sur plusieurs propriétés, créez plusieurs index à champ unique au lieu d'un index composé.",
"disclaimerCompoundIndexesLink": " Indices composés ", "disclaimerCompoundIndexesLink": " Indices composés ",
"disclaimerSuffix": "ne servent qu'à trier les résultats des requêtes. Si vous devez ajouter un index composé, vous pouvez en créer un à l'aide du shell Mongo.", "disclaimerSuffix": "ne servent qu'à trier les résultats des requêtes. Si vous devez ajouter un index composé, vous pouvez en créer un à l'aide de Mongo Shell.",
"compoundNotSupported": "Les collections avec index composés ne sont pas encore prises en charge dans l'éditeur d'indexation. Pour modifier la stratégie d'indexation de cette collection, utilisez le shell Mongo.", "compoundNotSupported": "Les collections avec index composés ne sont pas encore prises en charge dans l'éditeur d'indexation. Pour modifier la politique d'indexation de cette collection, utilisez le shell Mongo.",
"aadError": "Pour utiliser l'éditeur de stratégie d'indexation, veuillez vous connecter à", "aadError": "Pour utiliser l'éditeur de politique d'indexation, veuillez vous connecter à",
"aadErrorLink": "Portail Azure.", "aadErrorLink": "Portail Azure.",
"refreshingProgress": "Actualisation de la progression de la transformation de l'index", "refreshingProgress": "Actualisation de la progression de la transformation de l'index",
"canMakeMoreChangesZero": "Vous pourrez apporter d'autres modifications d'indexation une fois la transformation d'index actuelle terminée. ", "canMakeMoreChangesZero": "Vous pourrez apporter d'autres modifications d'indexation une fois la transformation d'index actuelle terminée. ",
@@ -909,7 +911,7 @@
"learnMorePrefix": "sur la manière de définir les propriétés calculées et de les utiliser." "learnMorePrefix": "sur la manière de définir les propriétés calculées et de les utiliser."
}, },
"indexingPolicy": { "indexingPolicy": {
"ariaLabel": "Stratégie d'indexation" "ariaLabel": "Politique d'indexation"
}, },
"dataMasking": { "dataMasking": {
"ariaLabel": "Stratégie de masquage des données", "ariaLabel": "Stratégie de masquage des données",
@@ -926,11 +928,11 @@
"globalSecondaryIndex": { "globalSecondaryIndex": {
"indexesDefined": "Ce conteneur possède les index suivants définis pour lui.", "indexesDefined": "Ce conteneur possède les index suivants définis pour lui.",
"learnMoreSuffix": "sur la manière de définir les index secondaires globaux et de les utiliser.", "learnMoreSuffix": "sur la manière de définir les index secondaires globaux et de les utiliser.",
"jsonAriaLabel": "Index secondaire mondial JSON", "jsonAriaLabel": "Index secondaire global JSON",
"addIndex": "Ajouter un index", "addIndex": "Ajouter un index",
"settingsTitle": "Paramètres d'indexation secondaire globale", "settingsTitle": "Paramètres de l'index secondaire global",
"sourceContainer": "Conteneur source", "sourceContainer": "Conteneur source",
"indexDefinition": "Définition globale des indices secondaires" "indexDefinition": "Définition de l'index secondaire global"
}, },
"indexingPolicyRefresh": { "indexingPolicyRefresh": {
"refreshFailed": "L'actualisation de la progression de la transformation de l'index a échoué" "refreshFailed": "L'actualisation de la progression de la transformation de l'index a échoué"
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "La taille en octets de quantification doit être supérieure à 0 et inférieure ou égale à 512", "quantizationByteSizeRangeError": "La taille en octets de quantification doit être supérieure à 0 et inférieure ou égale à 512",
"indexingSearchListSizeRangeError": "La taille de la liste de recherche dindexation doit être comprise entre 25 et 500 inclus" "indexingSearchListSizeRangeError": "La taille de la liste de recherche dindexation doit être comprise entre 25 et 500 inclus"
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Commentaires",
"feedbackButtonAriaLabel": "Fournir des commentaires sur les travaux de copie",
"refreshButtonAriaLabel": "Actualiser les travaux de copie",
"createCopyJobButtonLabel": "Créer un travail de copie",
"createCopyJobButtonAriaLabel": "Créer un travail de copie de conteneur"
},
"noCopyJobs": {
"title": "Aucun travail de copie à afficher",
"createCopyJobButtonText": "Créer un travail de copie de conteneur"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Détails du travail",
"errorTitle": "Détails de lerreur",
"selectedContainers": "Conteneurs sélectionnés"
},
"createCopyJob": {
"panelTitle": "Créer un travail de copie"
},
"selectAccount": {
"description": "Sélectionnez un compte de destination vers lequel effectuer la copie.",
"subscriptionDropdownLabel": "Abonnement",
"subscriptionDropdownPlaceholder": "Sélectionner un abonnement",
"accountDropdownLabel": "Compte",
"accountDropdownPlaceholder": "Sélectionner un compte"
},
"migrationType": {
"offline": {
"title": "Mode hors connexion",
"description": "Les travaux de copie hors connexion de conteneur vous permettent de copier des données dun conteneur source vers un conteneur Cosmos DB cible pour les API prises en charge. Pour garantir lintégrité des données entre la source et la destination, nous vous recommandons darrêter les mises à jour sur le conteneur source avant de créer le travail de copie. En savoir plus sur les [travaux de copie hors ligne](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql)."
},
"online": {
"title": "Mode en ligne",
"description": "Les travaux de copie en ligne de conteneur vous permettent de copier des données dun conteneur source vers un conteneur cible dAPI NoSQL de Cosmos DB à laide du flux de modification [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). Cela vous permet de continuer à mettre à jour la source pendant la copie des données. Un bref temps darrêt est nécessaire à la fin pour basculer en toute sécurité les applications clientes vers le conteneur cible. En savoir plus sur les [travaux de copie en ligne](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
"selectContainers": {
"description": "Veuillez sélectionner un conteneur source et un conteneur de destination vers lequel effectuer la copie.",
"sourceContainerSubHeading": "Conteneur source",
"targetContainerSubHeading": "Conteneur de destination",
"databaseDropdownLabel": "Base de données",
"databaseDropdownPlaceholder": "Sélectionner une base de données",
"containerDropdownLabel": "Conteneur",
"containerDropdownPlaceholder": "Sélectionner un conteneur",
"createNewContainerSubHeading": "Configurez les propriétés du nouveau conteneur sur le compte de destination « {{accountName}} ».",
"createNewContainerSubHeadingDefault": "Configurez les propriétés du nouveau conteneur.",
"createContainerButtonLabel": "Créer un conteneur",
"createContainerHeading": "Créer un conteneur"
},
"preview": {
"jobNameLabel": "Nom de travail",
"subscriptionLabel": "Abonnement de destination",
"accountLabel": "Compte de destination",
"sourceDatabaseLabel": "Base de données source",
"sourceContainerLabel": "Conteneur source",
"targetDatabaseLabel": "Base de données de destinations",
"targetContainerLabel": "Conteneur de destination"
},
"assignPermissions": {
"crossAccountDescription": "Pour copier des données de la source vers le conteneur de destination, vérifiez que lidentité managée du compte source dispose dun accès en lecture-écriture au compte de destination en effectuant les étapes suivantes.",
"intraAccountOnlineDescription": "Suivez les étapes ci-dessous pour activer la copie en ligne sur votre compte « {{accountName}} ».",
"crossAccountConfiguration": {
"title": "Copie de conteneur entre comptes",
"description": "Veuillez suivre les instructions ci-dessous pour accorder les autorisations nécessaires afin de copier des données de « {{sourceAccount}} » vers « {{destinationAccount}} »."
},
"onlineConfiguration": {
"title": "Copie de conteneur en ligne",
"description": "Suivre les instructions ci-dessous pour activer la copie en ligne sur votre compte « {{accountName}} »."
}
},
"popoverOverlaySpinnerLabel": "Veuillez patienter pendant que nous traitons votre requête...",
"addManagedIdentity": {
"title": "Identité managée affectée par le système activée.",
"description": "Une identité managée affectée par le système est limitée à une par ressource et est liée au cycle de vie de cette ressource. Une fois activée, vous pouvez accorder des autorisations à lidentité managée à laide du contrôle daccès en fonction du rôle Azure (RBAC Azure). Lidentité managée est authentifiée avec Microsoft Entra ID, si bien que vous navez pas à stocker dinformations didentification dans le code.",
"descriptionHrefText": "En savoir plus sur les identités managées.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "Identité managée affectée par le système",
"tooltipContent": "Découvrir plus dinformations sur",
"tooltipHrefText": "Identités managées.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "Vous pouvez sélectionner une identité managée attribuée par lutilisateur existante ou en créer une nouvelle.",
"userAssignedIdentityLabel": "Vous pouvez également sélectionner une identité managée affectée par lutilisateur(-trice).",
"createUserAssignedIdentityLink": "Créer une identité managée affectée par lutilisateur",
"enablementTitle": "Activer lidentité managée affectée par le système",
"enablementDescription": "Activer lidentité managée affectée par le système sur le {{accountName}}. Pour confirmer, cliquez sur le bouton « Oui »."
},
"defaultManagedIdentity": {
"title": "Identité managée affectée par le système définie par défaut.",
"description": "Définissez lidentité managée affectée par le système comme identité par défaut pour « {{accountName}} » en lactivant.",
"tooltipContent": "Découvrir plus dinformations sur",
"tooltipHrefText": "Identités managées par défaut.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "Identité managée affectée par le système définie par défaut",
"popoverDescription": "Attribuer lidentité managée affectée par le système comme identité par défaut pour « {{accountName}} ». Pour confirmer, cliquez sur le bouton « Oui ». "
},
"readWritePermissionAssigned": {
"title": "Autorisations en lecture-écriture attribuées à lidentité par défaut.",
"description": "Pour autoriser la copie des données de la source vers le conteneur de destination, accordez un accès en lecture-écriture au compte de destination à lidentité par défaut du compte source.",
"tooltipContent": "Découvrir plus dinformations sur",
"tooltipHrefText": "Autorisation de lecture et d’écriture.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Attribuer des autorisations de lecture et d’écriture à lidentité par défaut.",
"popoverDescription": "Attribuer des autorisations de lecture-écriture sur le compte de destination à lidentité par défaut du compte source. Pour confirmer, cliquez sur le bouton « Oui »."
},
"pointInTimeRestore": {
"title": "Restauration à un moment donné activée",
"description": "Pour faciliter les opérations de copie de conteneur en ligne, mettez à jour la stratégie de sauvegarde de votre compte « {{accessName}} » de la sauvegarde périodique à la sauvegarde continue. Lactivation de la sauvegarde continue est requise pour cette fonctionnalité.",
"tooltipContent": "Découvrir plus dinformations sur",
"tooltipHrefText": "Sauvegarde continue",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "Activer la restauration à un moment donné"
},
"onlineCopyEnabled": {
"title": "Copie en ligne activée",
"description": "Activez la copie de conteneur en ligne en cliquant sur le bouton ci-dessous dans votre compte « {{accountName}} ».",
"hrefText": "En savoir plus sur les travaux de copie en ligne",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "Activer la copie en ligne",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Validation en cours du Mode de flux de modification de toutes les versions et suppressions (préversion)",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Activation en cours du Mode de flux de modification de toutes les versions et suppressions (préversion)...",
"enablingOnlineCopySpinnerLabel": "Activation de la copie en ligne sur votre compte « {{accountName}} » ..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "Date et heure",
"name": "Nom de travail",
"status": "Statut",
"completionPercentage": "% dachèvement",
"duration": "Durée",
"error": "Message derreur",
"mode": "Mode",
"actions": "Actions"
},
"actions": {
"pause": "Suspendre",
"resume": "Reprendre",
"complete": "Terminé",
"viewDetails": "Afficher les détails"
},
"status": {
"pending": "En file dattente",
"inProgress": "Exécution",
"running": "Exécution",
"partitioning": "Exécution",
"paused": "En pause",
"completed": "Terminé",
"failed": "Échec",
"faulted": "Échec",
"skipped": "Annulé",
"cancelled": "Annulé"
},
"dialog": {
"confirmButtonText": "Confirmer",
"cancelButtonText": "Annuler"
}
}
} }
} }
+169 -8
View File
@@ -34,6 +34,8 @@
"browse": "Tallózás", "browse": "Tallózás",
"increaseValueBy1": "Érték növelése 1-gyel", "increaseValueBy1": "Érték növelése 1-gyel",
"decreaseValueBy1": "Érték csökkentése 1-gyel", "decreaseValueBy1": "Érték csökkentése 1-gyel",
"on": "Be",
"off": "Ki",
"preview": "Előnézet" "preview": "Előnézet"
}, },
"splashScreen": { "splashScreen": {
@@ -303,7 +305,7 @@
"deleteContainer": "{{containerName}} törlése", "deleteContainer": "{{containerName}} törlése",
"newSqlQuery": "Új SQL-lekérdezés", "newSqlQuery": "Új SQL-lekérdezés",
"newQuery": "Új lekérdezés", "newQuery": "Új lekérdezés",
"openMongoShell": "Mongo-felület megnyitása", "openMongoShell": "Mongo Shell megnyitása",
"newShell": "Új felület", "newShell": "Új felület",
"openCassandraShell": "Cassandra-felület megnyitása", "openCassandraShell": "Cassandra-felület megnyitása",
"newStoredProcedure": "Új tárolt eljárás", "newStoredProcedure": "Új tárolt eljárás",
@@ -414,7 +416,7 @@
"refreshGridFailed": "Nem sikerült frissíteni a dokumentumrácsot" "refreshGridFailed": "Nem sikerült frissíteni a dokumentumrácsot"
}, },
"mongoShell": { "mongoShell": {
"title": "Mongo-felület" "title": "Mongo Shell"
} }
}, },
"panes": { "panes": {
@@ -488,7 +490,7 @@
"legacySdkInfo": "A régebbi SDK-kkal való kompatibilitás biztosítása érdekében a létrehozott tároló egy örökölt particionálási sémát fog használni, amely legfeljebb 101 bájt méretű partíciókulcs-értékeket támogat. Ha ez engedélyezve van, nem használhat hierarchikus partíciókulcsokat.", "legacySdkInfo": "A régebbi SDK-kkal való kompatibilitás biztosítása érdekében a létrehozott tároló egy örökölt particionálási sémát fog használni, amely legfeljebb 101 bájt méretű partíciókulcs-értékeket támogat. Ha ez engedélyezve van, nem használhat hierarchikus partíciókulcsokat.",
"indexingOnInfo": "A dokumentumok összes tulajdonsága alapértelmezés szerint indexelve lesz a rugalmas és hatékony lekérdezések érdekében.", "indexingOnInfo": "A dokumentumok összes tulajdonsága alapértelmezés szerint indexelve lesz a rugalmas és hatékony lekérdezések érdekében.",
"indexingOffInfo": "Az indexelés ki lesz kapcsolva. Akkor ajánlott, ha nem kell lekérdezéseket futtatnia, vagy csak kulcsérték-műveletekkel rendelkezik.", "indexingOffInfo": "Az indexelés ki lesz kapcsolva. Akkor ajánlott, ha nem kell lekérdezéseket futtatnia, vagy csak kulcsérték-műveletekkel rendelkezik.",
"indexingOffWarning": "Ha úgy hozza létre ezt a tárolót, hogy az indexelés ki van kapcsolva, nem fogja tudni módosítani az indexelési szabályzatot. Az indexelési módosítások csak indexelési szabályzattal rendelkező tárolókon engedélyezettek.", "indexingOffWarning": "Ha úgy hozza létre ezt a tárolót, hogy az indexelés ki van kapcsolva, nem fogja tudni módosítani az indexelési házirendet. Az indexelési módosítások csak indexelési szabályzattal rendelkező tárolókon engedélyezettek.",
"acknowledgeSpendErrorMonthly": "Nyugtázza a becsült havi ráfordítást.", "acknowledgeSpendErrorMonthly": "Nyugtázza a becsült havi ráfordítást.",
"acknowledgeSpendErrorDaily": "Nyugtázza a becsült napi ráfordítást.", "acknowledgeSpendErrorDaily": "Nyugtázza a becsült napi ráfordítást.",
"unshardedMaxRuError": "A horizontálisan nem skálázott gyűjtemények legfeljebb 10 000 kérelemegységet támogatnak", "unshardedMaxRuError": "A horizontálisan nem skálázott gyűjtemények legfeljebb 10 000 kérelemegységet támogatnak",
@@ -619,7 +621,7 @@
"accountId": "Fiókazonosító", "accountId": "Fiókazonosító",
"sessionId": "Munkamenet-azonosító", "sessionId": "Munkamenet-azonosító",
"popupsDisabledError": "Nem sikerült hitelesítést létrehozni ehhez a fiókhoz, mert a böngészőben le vannak tiltva az előugró ablakok.\nEngedélyezze az előugró ablakokat ehhez a webhelyhez, majd kattintson az Entra ID bejelentkezés gombra", "popupsDisabledError": "Nem sikerült hitelesítést létrehozni ehhez a fiókhoz, mert a böngészőben le vannak tiltva az előugró ablakok.\nEngedélyezze az előugró ablakokat ehhez a webhelyhez, majd kattintson az Entra ID bejelentkezés gombra",
"failedToAcquireTokenError": "Nem sikerült automatikusan beszerezni az engedélyezési jogkivonatot. Kattintson az Entra ID bejelentkezés gombra az Entra ID RBAC-műveletek engedélyezéséhez" "failedToAcquireTokenError": "Nem sikerült automatikusan beszerezni a hozzáférési jogkivonatot. Kattintson az Entra ID bejelentkezés gombra az Entra ID RBAC-műveletek engedélyezéséhez"
}, },
"saveQuery": { "saveQuery": {
"panelTitle": "Lekérdezés mentése", "panelTitle": "Lekérdezés mentése",
@@ -756,13 +758,13 @@
"scale": "Skálázás", "scale": "Skálázás",
"conflictResolution": "Ütközésfeloldás", "conflictResolution": "Ütközésfeloldás",
"settings": "Beállítások", "settings": "Beállítások",
"indexingPolicy": "Indexelési szabályzat", "indexingPolicy": "Indexelési házirend",
"partitionKeys": "Partíciókulcsok", "partitionKeys": "Partíciókulcsok",
"partitionKeysPreview": "Partíciókulcsok (előzetes verzió)", "partitionKeysPreview": "Partíciókulcsok (előzetes verzió)",
"computedProperties": "Számított tulajdonságok", "computedProperties": "Számított tulajdonságok",
"containerPolicies": "Tárolószabályzatok", "containerPolicies": "Tárolószabályzatok",
"throughputBuckets": "Átviteli sebesség gyűjtői", "throughputBuckets": "Átviteli sebesség gyűjtői",
"globalSecondaryIndexPreview": "Globális másodlagos index (előzetes verzió)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "Maszkolási szabályzat" "maskingPolicyPreview": "Maszkolási szabályzat"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -841,7 +843,7 @@
"mongoIndexing": { "mongoIndexing": {
"disclaimer": "Több tulajdonságra szűrő lekérdezések esetén összetett index helyett hozzon létre több egymezős indexet.", "disclaimer": "Több tulajdonságra szűrő lekérdezések esetén összetett index helyett hozzon létre több egymezős indexet.",
"disclaimerCompoundIndexesLink": " Összetett indexek ", "disclaimerCompoundIndexesLink": " Összetett indexek ",
"disclaimerSuffix": "csak a lekérdezési eredmények rendezésére szolgál. Ha összetett indexet kell hozzáadnia, a Mongo-felület használatával hozhat létre egyet.", "disclaimerSuffix": "csak a lekérdezési eredmények rendezésére szolgál. Ha összetett indexet kell hozzáadnia, a Mongo Shell használatával hozhat létre egyet.",
"compoundNotSupported": "Az összetett indexekkel rendelkező gyűjtemények még nem támogatottak az indexelőszerkesztőben. A gyűjtemény indexelési házirendjének módosításához használja a Mongo-felületet.", "compoundNotSupported": "Az összetett indexekkel rendelkező gyűjtemények még nem támogatottak az indexelőszerkesztőben. A gyűjtemény indexelési házirendjének módosításához használja a Mongo-felületet.",
"aadError": "Az indexelési házirendszerkesztő használatához jelentkezzen be:", "aadError": "Az indexelési házirendszerkesztő használatához jelentkezzen be:",
"aadErrorLink": "Azure Portal.", "aadErrorLink": "Azure Portal.",
@@ -909,7 +911,7 @@
"learnMorePrefix": "a számított tulajdonságok definiálásáról és azok használatáról." "learnMorePrefix": "a számított tulajdonságok definiálásáról és azok használatáról."
}, },
"indexingPolicy": { "indexingPolicy": {
"ariaLabel": "Indexelési szabályzat" "ariaLabel": "Indexelési házirend"
}, },
"dataMasking": { "dataMasking": {
"ariaLabel": "Adatmaszkolási házirend", "ariaLabel": "Adatmaszkolási házirend",
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "A kvantálási bájtméretnek nagyobbnak kell lennie 0-nál, és legfeljebb 512 lehet", "quantizationByteSizeRangeError": "A kvantálási bájtméretnek nagyobbnak kell lennie 0-nál, és legfeljebb 512 lehet",
"indexingSearchListSizeRangeError": "Az indexelő keresési lista méretének 25-tel egyenlőnek vagy annál nagyobbnak és ugyanakkor 500-nál kisebbnek vagy azzal egyenlőnek kell lennie" "indexingSearchListSizeRangeError": "Az indexelő keresési lista méretének 25-tel egyenlőnek vagy annál nagyobbnak és ugyanakkor 500-nál kisebbnek vagy azzal egyenlőnek kell lennie"
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Visszajelzés",
"feedbackButtonAriaLabel": "Visszajelzés küldése a másolási feladatokról",
"refreshButtonAriaLabel": "Másolási feladatok frissítése",
"createCopyJobButtonLabel": "Másolási feladat létrehozása",
"createCopyJobButtonAriaLabel": "Új tárolómásolási feladat létrehozása"
},
"noCopyJobs": {
"title": "Nincsenek megjeleníthető másolási feladatok",
"createCopyJobButtonText": "Tárolómásolási feladat létrehozása"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Feladat részletei",
"errorTitle": "Hiba részletei",
"selectedContainers": "Kiválasztott tárolók"
},
"createCopyJob": {
"panelTitle": "Másolási feladat létrehozása"
},
"selectAccount": {
"description": "Válassza ki a célfiókot, amelybe másolni szeretne.",
"subscriptionDropdownLabel": "Előfizetés",
"subscriptionDropdownPlaceholder": "Előfizetés kiválasztása",
"accountDropdownLabel": "Fiók",
"accountDropdownPlaceholder": "Fiók kiválasztása"
},
"migrationType": {
"offline": {
"title": "Offline mód",
"description": "Az offline tárolómásolási feladatok lehetővé teszik az adatok másolását egy forrástárolóból egy cél Cosmos DB-tárolóba a támogatott API-k esetében. Az adatintegritás megőrzése érdekében a forrás és a cél között javasoljuk, hogy a másolási feladat létrehozása előtt állítsa le a forrástároló frissítéseit. További információ az [offline másolási feladatokról](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql)."
},
"online": {
"title": "Online üzemmód",
"description": "Az online tárolómásolási feladatok lehetővé teszik, hogy adatokat másoljon egy forrástárolóból egy cél Cosmos DB NoSQL API-tárolóba a [Minden verzió és törlés](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) változáscsatorna használatával. Ez lehetővé teszi, hogy a frissítések az adatok másolása közben is folytatódjanak a forráson. A végén rövid leállásra van szükség ahhoz, hogy biztonságosan átválthassa az ügyfélalkalmazásokat a céltárolóra. További információ az [online másolási feladatokról](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
"selectContainers": {
"description": "Válasszon ki egy forrástárolót és egy céltárolót, amelybe másolni szeretne.",
"sourceContainerSubHeading": "Forrástároló",
"targetContainerSubHeading": "Céloldali tároló",
"databaseDropdownLabel": "Adatbázis",
"databaseDropdownPlaceholder": "Adatbázis kiválasztása",
"containerDropdownLabel": "Tároló",
"containerDropdownPlaceholder": "Válasszon ki egy tárolót",
"createNewContainerSubHeading": "Konfigurálja az új tároló tulajdonságait a(z) „{{accountName}}” célfiókban.",
"createNewContainerSubHeadingDefault": "Konfigurálja az új tároló tulajdonságait.",
"createContainerButtonLabel": "Új tároló létrehozása",
"createContainerHeading": "Új tároló létrehozása"
},
"preview": {
"jobNameLabel": "Feladat neve",
"subscriptionLabel": "Célelőfizetés",
"accountLabel": "Célfiók",
"sourceDatabaseLabel": "Forrásadatbázis",
"sourceContainerLabel": "Forrástároló",
"targetDatabaseLabel": "Céladatbázis neve: {0}",
"targetContainerLabel": "Céloldali tároló"
},
"assignPermissions": {
"crossAccountDescription": "Ahhoz, hogy adatokat másolhasson a forrásból a céltárolóba, az alábbi lépések végrehajtásával győződjön meg arról, hogy a forrásfiók felügyelt identitása olvasási-írási hozzáféréssel rendelkezik a célfiókhoz.",
"intraAccountOnlineDescription": "Kövesse az alábbi lépéseket az online másolás engedélyezéséhez a(z) „{{accountName}}” fiókjában.",
"crossAccountConfiguration": {
"title": "Fiókközi tárolómásolás",
"description": "Kövesse az alábbi utasítást ahhoz, hogy megadja a(z) „{{sourceAccount}}” helyről a(z) „{{destinationAccount}}” helyre végzett másoláshoz szükséges engedélyeket."
},
"onlineConfiguration": {
"title": "Online tárolómásolás",
"description": "Kövesse az alábbi utasításokat az online másolás engedélyezéséhez a(z) „{{accountName}}” fiókjában."
}
},
"popoverOverlaySpinnerLabel": "Kis türelmet, amíg feldolgozzuk a kérését...",
"addManagedIdentity": {
"title": "Rendszer által hozzárendelt felügyelt identitás engedélyezve.",
"description": "Erőforrásonként csak egy rendszer által hozzárendelt felügyelt identitás adható meg, és ennek életciklusa megegyezik az adott erőforráséval. Az engedélyezés után a felügyelt identitáshoz az Azure szerepköralapú hozzáférés-vezérlési (Azure RBAC) funkciójával rendelhet hozzá engedélyeket. A felügyelt identitást a Microsoft Entra ID hitelesíti, így nem kell hitelesítő adatokat tárolnia a programkódban.",
"descriptionHrefText": "További információ a felügyelt identitásokról.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "Rendszer által hozzárendelt felügyelt identitás",
"tooltipContent": "További információ",
"tooltipHrefText": "Felügyelt identitások.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "Kiválaszthat egy meglévő felhasználó által hozzárendelt identitást, vagy létrehozhat egy újat.",
"userAssignedIdentityLabel": "Felhasználó által hozzárendelt felügyelt identitást is választhat.",
"createUserAssignedIdentityLink": "Felhasználó által hozzárendelt felügyelt identitás létrehozása",
"enablementTitle": "Rendszer által hozzárendelt felügyelt identitás engedélyezése",
"enablementDescription": "Rendszer által hozzárendelt felügyelt identitás engedélyezése a következőn: {{accountName}}. A megerősítéshez kattintson az „Igen” gombra."
},
"defaultManagedIdentity": {
"title": "Rendszer által hozzárendelt felügyelt identitás alapértelmezettként beállítva.",
"description": "Állítsa be a rendszer által hozzárendelt felügyelt identitást alapértelmezettként a(z) „{{accountName}}” fiókban a bekapcsolásával.",
"tooltipContent": "További információ",
"tooltipHrefText": "Alapértelmezett felügyelt identitások.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "Rendszer által hozzárendelt felügyelt identitás alapértelmezettként beállítva",
"popoverDescription": "Rendelje hozzá alapértelmezettként a rendszer által hozzárendelt felügyelt identitást ehhez: „{{accountName}}”. A megerősítéshez kattintson az „Igen” gombra. "
},
"readWritePermissionAssigned": {
"title": "Az alapértelmezett identitáshoz rendelt olvasási-írási engedélyek.",
"description": "Ahhoz, hogy az adatokat a forrásból a céltárolóba másolhassa, adjon olvasási-írási hozzáférést a célfiókhoz a forrásfiók alapértelmezett identitása számára.",
"tooltipContent": "További információ",
"tooltipHrefText": "Olvasási-írási engedélyek.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Olvasási és írási engedélyek hozzárendelése az alapértelmezett identitáshoz.",
"popoverDescription": "Rendeljen olvasási-írási engedélyeket a célfiókhoz a forrásfiók alapértelmezett identitása számára. A megerősítéshez kattintson az „Igen” gombra."
},
"pointInTimeRestore": {
"title": "Időponthoz kötött helyreállítás engedélyezve",
"description": "Az online tárolómásolási feladatok támogatásához frissítse a(z) „{{accessName}}” biztonsági mentési szabályzatát időszakosról folyamatos biztonsági mentésre. A folyamatos biztonsági mentést engedélyezni kell ehhez a funkcióhoz.",
"tooltipContent": "További információ",
"tooltipHrefText": "Folyamatos biztonsági mentés",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "Időponthoz kötött helyreállítás engedélyezése"
},
"onlineCopyEnabled": {
"title": "Online másolás engedélyezve",
"description": "Engedélyezze az online tárolómásolást az alábbi gombra kattintva a(z) „{{accountName}}” fiókjában.",
"hrefText": "További információ az online másolási feladatokról",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "Online másolás engedélyezése",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Az összes verzió és a törlések változáscsatorna-módjának ellenőrzése",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Az összes verzió és a törlések változáscsatorna-módjának engedélyezése...",
"enablingOnlineCopySpinnerLabel": "Az online másolás engedélyezése a(z) „{{accountName}}” fiókban ..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "Dátum és idő",
"name": "Feladat neve",
"status": "Állapot",
"completionPercentage": "Befejezés %",
"duration": "Időtartam",
"error": "Hibaüzenet",
"mode": "Mód",
"actions": "Műveletek"
},
"actions": {
"pause": "Szüneteltetés",
"resume": "Folytatás",
"complete": "Kész",
"viewDetails": "Részletek megtekintése"
},
"status": {
"pending": "Várólistára helyezve",
"inProgress": "Fut",
"running": "Fut",
"partitioning": "Fut",
"paused": "Felfüggesztve",
"completed": "Kész",
"failed": "Hibás",
"faulted": "Hibás",
"skipped": "Megszakítva",
"cancelled": "Megszakítva"
},
"dialog": {
"confirmButtonText": "Megerősítés",
"cancelButtonText": "Mégse"
}
}
} }
} }
+164 -3
View File
@@ -34,6 +34,8 @@
"browse": "Telusuri", "browse": "Telusuri",
"increaseValueBy1": "Tambah nilai sebesar 1", "increaseValueBy1": "Tambah nilai sebesar 1",
"decreaseValueBy1": "Kurangi nilai sebesar 1", "decreaseValueBy1": "Kurangi nilai sebesar 1",
"on": "Aktif",
"off": "Nonaktif",
"preview": "Pratinjau" "preview": "Pratinjau"
}, },
"splashScreen": { "splashScreen": {
@@ -762,7 +764,7 @@
"computedProperties": "Properti Terkomputasi", "computedProperties": "Properti Terkomputasi",
"containerPolicies": "Kebijakan Kontainer", "containerPolicies": "Kebijakan Kontainer",
"throughputBuckets": "Wadah Throughput", "throughputBuckets": "Wadah Throughput",
"globalSecondaryIndexPreview": "Indeks Sekunder Global (Pratinjau)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "Kebijakan Masking" "maskingPolicyPreview": "Kebijakan Masking"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -811,8 +813,8 @@
"quotaMaxOption": "Maksimum kuota Anda saat ini adalah {{maximumThroughput}} RU/dtk. Untuk menambah di atas batas ini, Anda harus mengajukan permintaan penambahan kuota dan tim Azure Cosmos DB akan meninjaunya.", "quotaMaxOption": "Maksimum kuota Anda saat ini adalah {{maximumThroughput}} RU/dtk. Untuk menambah di atas batas ini, Anda harus mengajukan permintaan penambahan kuota dan tim Azure Cosmos DB akan meninjaunya.",
"belowMinimumMessage": "Anda tidak dapat menurunkan throughput di bawah minimum {{minimum}} RU/dtk saat ini. Untuk informasi selengkapnya tentang batas ini, lihat dokumentasi kutipan layanan kami.", "belowMinimumMessage": "Anda tidak dapat menurunkan throughput di bawah minimum {{minimum}} RU/dtk saat ini. Untuk informasi selengkapnya tentang batas ini, lihat dokumentasi kutipan layanan kami.",
"saveThroughputWarning": "Tagihan Anda akan terpengaruh jika Anda memperbarui pengaturan throughput. Periksa perkiraan biaya yang baru di bawah ini sebelum menyimpan perubahan Anda", "saveThroughputWarning": "Tagihan Anda akan terpengaruh jika Anda memperbarui pengaturan throughput. Periksa perkiraan biaya yang baru di bawah ini sebelum menyimpan perubahan Anda",
"currentAutoscaleThroughput": "Throughput skala otomatis saat ini:", "currentAutoscaleThroughput": "Throughput penskalaan otomatis saat ini:",
"targetAutoscaleThroughput": "Throughput skala otomatis target:", "targetAutoscaleThroughput": "Throughput penskalaan otomatis target:",
"currentManualThroughput": "Throughput manual saat ini:", "currentManualThroughput": "Throughput manual saat ini:",
"targetManualThroughput": "Throughput manual target:", "targetManualThroughput": "Throughput manual target:",
"applyDelayedMessage": "Permintaan untuk menambah throughput berhasil dikirimkan. Operasi ini akan memakan waktu 1-3 hari kerja. Tampilkan status terbaru di Pemberitahuan.", "applyDelayedMessage": "Permintaan untuk menambah throughput berhasil dikirimkan. Operasi ini akan memakan waktu 1-3 hari kerja. Tampilkan status terbaru di Pemberitahuan.",
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "Ukuran byte kuantisasi harus lebih besar dari 0 dan kurang dari atau sama dengan 512", "quantizationByteSizeRangeError": "Ukuran byte kuantisasi harus lebih besar dari 0 dan kurang dari atau sama dengan 512",
"indexingSearchListSizeRangeError": "Ukuran daftar pencarian pengindeks harus lebih besar dari atau sama dengan 25 dan kurang dari atau sama dengan 500" "indexingSearchListSizeRangeError": "Ukuran daftar pencarian pengindeks harus lebih besar dari atau sama dengan 25 dan kurang dari atau sama dengan 500"
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Umpan balik",
"feedbackButtonAriaLabel": "Berikan umpan balik tentang pekerjaan penyalinan",
"refreshButtonAriaLabel": "Refresh pekerjaan penyalinan",
"createCopyJobButtonLabel": "Buat Pekerjaan Penyalinan",
"createCopyJobButtonAriaLabel": "Buat pekerjaan penyalinan kontainer baru"
},
"noCopyJobs": {
"title": "Tidak ada pekerjaan penyalinan untuk ditampilkan",
"createCopyJobButtonText": "Buat pekerjaan penyalinan kontainer"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Detail Pekerjaan",
"errorTitle": "Detail Kesalahan",
"selectedContainers": "Kontainer yang Dipilih"
},
"createCopyJob": {
"panelTitle": "Buat pekerjaan penyalinan"
},
"selectAccount": {
"description": "Pilih akun tujuan untuk penyalinan.",
"subscriptionDropdownLabel": "Langganan",
"subscriptionDropdownPlaceholder": "Pilih langganan",
"accountDropdownLabel": "Akun",
"accountDropdownPlaceholder": "Pilih akun"
},
"migrationType": {
"offline": {
"title": "Modus offline",
"description": "Pekerjaan penyalinan kontainer offline memungkinkan Anda menyalin data dari kontainer sumber ke kontainer Cosmos DB tujuan untuk API yang didukung. Untuk memastikan integritas data antara sumber dan tujuan, sebaiknya hentikan pembaruan pada kontainer sumber sebelum membuat pekerjaan penyalinan. Pelajari selengkapnya tentang [pekerjaan penyalinan offline](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql)."
},
"online": {
"title": "Mode online",
"description": "Pekerjaan penyalinan kontainer online memungkinkan Anda menyalin data dari kontainer sumber ke kontainer API Cosmos DB NoSQL tujuan menggunakan umpan perubahan [Semua Versi dan Hapus](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Hal ini memungkinkan pembaruan untuk melanjutkan sumber saat data disalin. Waktu henti singkat diperlukan di akhir untuk mengalihkan aplikasi klien ke kontainer tujuan dengan aman. Pelajari selengkapnya tentang [pekerjaan penyalinan online](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
"selectContainers": {
"description": "Pilih kontainer sumber dan kontainer tujuan yang akan digunakan untuk penyalinan.",
"sourceContainerSubHeading": "Kontainer sumber",
"targetContainerSubHeading": "Kontainer tujuan",
"databaseDropdownLabel": "Database",
"databaseDropdownPlaceholder": "Pilih database",
"containerDropdownLabel": "Kontainer",
"containerDropdownPlaceholder": "Pilih kontainer",
"createNewContainerSubHeading": "Konfigurasikan properti untuk kontainer baru pada akun tujuan \"{{accountName}}\".",
"createNewContainerSubHeadingDefault": "Konfigurasikan properti untuk kontainer baru.",
"createContainerButtonLabel": "Buat kontainer baru",
"createContainerHeading": "Buat kontainer baru"
},
"preview": {
"jobNameLabel": "Nama pekerjaan",
"subscriptionLabel": "Langganan tujuan",
"accountLabel": "Akun tujuan",
"sourceDatabaseLabel": "Database sumber",
"sourceContainerLabel": "Kontainer sumber",
"targetDatabaseLabel": "Database tujuan",
"targetContainerLabel": "Kontainer tujuan"
},
"assignPermissions": {
"crossAccountDescription": "Untuk menyalin data dari sumber ke kontainer tujuan, pastikan bahwa identitas terkelola akun sumber memiliki akses baca-tulis ke akun tujuan dengan menyelesaikan langkah-langkah berikut.",
"intraAccountOnlineDescription": "Ikuti langkah-langkah di bawah ini untuk mengaktifkan penyalinan online di akun \"{{accountName}}\".",
"crossAccountConfiguration": {
"title": "Salinan kontainer lintas akun",
"description": "Ikuti instruksi di bawah ini untuk memberikan izin yang diperlukan untuk menyalin data dari \"{{sourceAccount}}\" ke \"{{destinationAccount}}\"."
},
"onlineConfiguration": {
"title": "Penyalinan kontainer online",
"description": "Ikuti petunjuk di bawah ini untuk mengaktifkan penyalinan online di akun \"{{accountName}}\" Anda."
}
},
"popoverOverlaySpinnerLabel": "Harap tunggu sementara kami memproses permintaan Anda...",
"addManagedIdentity": {
"title": "Identitas terkelola yang ditetapkan sistem diaktifkan.",
"description": "Identitas terkelola yang ditetapkan sistem dibatasi hanya satu identitas per sumber daya dan dikaitkan dengan siklus hidup sumber daya ini. Setelah diaktifkan, Anda dapat memberikan izin ke identitas terkelola menggunakan kontrol akses berbasis peran Azure (Azure RBAC). Identitas terkelola diautentikasi dengan Microsoft Entra ID sehingga Anda tidak perlu menyimpan kredensial apa pun dalam kode.",
"descriptionHrefText": "Pelajari selengkapnya tentang Identitas terkelola.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "Identitas terkelola yang ditetapkan sistem",
"tooltipContent": "Pelajari selengkapnya tentang",
"tooltipHrefText": "Identitas Terkelola.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "Anda dapat memilih identitas yang ditetapkan pengguna yang ada atau membuat identitas baru.",
"userAssignedIdentityLabel": "Anda juga dapat memilih identitas terkelola yang ditetapkan pengguna.",
"createUserAssignedIdentityLink": "Buat Identitas Terkelola yang Ditetapkan Pengguna",
"enablementTitle": "Aktifkan identitas terkelola yang ditetapkan sistem",
"enablementDescription": "Aktifkan identitas terkelola yang ditetapkan sistem pada {{accountName}}. Untuk mengonfirmasi, klik tombol \"Ya\"."
},
"defaultManagedIdentity": {
"title": "Identitas terkelola yang ditetapkan sistem ditetapkan sebagai default.",
"description": "Atur identitas terkelola yang ditetapkan sistem sebagai default untuk \"{{accountName}}\" dengan mengaktifkannya.",
"tooltipContent": "Pelajari selengkapnya tentang",
"tooltipHrefText": "Identitas Terkelola Default.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "Identitas terkelola yang ditetapkan sistem ditetapkan sebagai default",
"popoverDescription": "Tetapkan identitas terkelola yang ditetapkan sistem sebagai default untuk \"{{accountName}}\". Untuk mengonfirmasi, klik tombol \"Ya\". "
},
"readWritePermissionAssigned": {
"title": "Izin baca-tulis ditetapkan ke identitas default.",
"description": "Untuk mengizinkan penyalinan data dari sumber ke kontainer tujuan, berikan akses baca-tulis pada akun tujuan ke identitas default akun sumber.",
"tooltipContent": "Pelajari selengkapnya tentang",
"tooltipHrefText": "Izin baca-tulis.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Tetapkan izin baca-tulis ke identitas default.",
"popoverDescription": "Tetapkan izin baca-tulis pada akun tujuan ke identitas default akun sumber. Untuk mengonfirmasi, klik tombol \"Ya\"."
},
"pointInTimeRestore": {
"title": "Pemulihan Titik Waktu diaktifkan",
"description": "Untuk memfasilitasi pekerjaan penyalinan kontainer online, perbarui kebijakan pencadangan \"{{accessName}}\" Anda dari berkala menjadi pencadangan berkelanjutan. Pengaktifan pencadangan berkelanjutan diperlukan untuk fungsionalitas ini.",
"tooltipContent": "Pelajari selengkapnya tentang",
"tooltipHrefText": "Pencadangan Berkelanjutan",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "Aktifkan Pemulihan Titik Waktu"
},
"onlineCopyEnabled": {
"title": "Penyalinan online diaktifkan",
"description": "Aktifkan penyalinan kontainer online dengan mengeklik tombol di bawah ini pada akun \"{{accountName}}\".",
"hrefText": "Pelajari selengkapnya tentang pekerjaan penyalinan online",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "Aktifkan Penyalinan Online",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Memvalidasi Semua versi dan penghapusan mengubah mode umpan (pratinjau)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Mengaktifkan Semua versi dan menghapus mengubah mode umpan (pratinjau)...",
"enablingOnlineCopySpinnerLabel": "Mengaktifkan penyalinan online di akun \"{{accountName}}\"..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "Tanggal dan waktu",
"name": "Nama pekerjaan",
"status": "Status",
"completionPercentage": "% Penyelesaian",
"duration": "Durasi",
"error": "Pesan kesalahan",
"mode": "Mode",
"actions": "Tindakan"
},
"actions": {
"pause": "Jeda",
"resume": "Lanjutkan",
"complete": "Selesai",
"viewDetails": "Tampilkan Detail"
},
"status": {
"pending": "Diantrekan",
"inProgress": "Berjalan",
"running": "Berjalan",
"partitioning": "Berjalan",
"paused": "Dijeda",
"completed": "Selesai",
"failed": "Gagal",
"faulted": "Gagal",
"skipped": "Dibatalkan",
"cancelled": "Dibatalkan"
},
"dialog": {
"confirmButtonText": "Konfirmasikan",
"cancelButtonText": "Batal"
}
}
} }
} }
+191 -30
View File
@@ -34,6 +34,8 @@
"browse": "Sfoglia", "browse": "Sfoglia",
"increaseValueBy1": "Aumentare il valore di 1", "increaseValueBy1": "Aumentare il valore di 1",
"decreaseValueBy1": "Diminuisci il valore di 1", "decreaseValueBy1": "Diminuisci il valore di 1",
"on": "Attivato",
"off": "Disattivato",
"preview": "Anteprima" "preview": "Anteprima"
}, },
"splashScreen": { "splashScreen": {
@@ -76,7 +78,7 @@
"description": "Creare una tabella e interagire con i dati usando l'interfaccia shell di PostgreSQL" "description": "Creare una tabella e interagire con i dati usando l'interfaccia shell di PostgreSQL"
}, },
"vcoreMongo": { "vcoreMongo": {
"title": "Shell Mongo", "title": "Mongo Shell",
"description": "Creare una raccolta e interagire con i dati usando l'interfaccia shell di MongoDB" "description": "Creare una raccolta e interagire con i dati usando l'interfaccia shell di MongoDB"
} }
}, },
@@ -303,7 +305,7 @@
"deleteContainer": "Elimina {{containerName}}", "deleteContainer": "Elimina {{containerName}}",
"newSqlQuery": "Nuova query SQL", "newSqlQuery": "Nuova query SQL",
"newQuery": "Nuova query", "newQuery": "Nuova query",
"openMongoShell": "Apri shell Mongo", "openMongoShell": "Apri Mongo Shell",
"newShell": "Nuova Shell", "newShell": "Nuova Shell",
"openCassandraShell": "Apri shell Cassandra", "openCassandraShell": "Apri shell Cassandra",
"newStoredProcedure": "Nuova stored procedure", "newStoredProcedure": "Nuova stored procedure",
@@ -414,7 +416,7 @@
"refreshGridFailed": "Aggiornamento griglia documenti non riuscito" "refreshGridFailed": "Aggiornamento griglia documenti non riuscito"
}, },
"mongoShell": { "mongoShell": {
"title": "Shell Mongo" "title": "Mongo Shell"
} }
}, },
"panes": { "panes": {
@@ -442,7 +444,7 @@
"keyspaceIdLabel": "ID keyspace", "keyspaceIdLabel": "ID keyspace",
"databaseIdPlaceholder": "Digitare un nuovo ID {{databaseLabel}}", "databaseIdPlaceholder": "Digitare un nuovo ID {{databaseLabel}}",
"databaseTooltip": "Un {{databaseLabel}} è un contenitore logico di una o più {{collectionsLabel}}", "databaseTooltip": "Un {{databaseLabel}} è un contenitore logico di una o più {{collectionsLabel}}",
"greaterThanError": "Immettere un valore maggiore di {{minValue}} per la velocità effettiva di Autopilot", "greaterThanError": "Immetti un valore maggiore di {{minValue}} per la velocità effettiva di Autopilot",
"acknowledgeSpendError": "Confermare la spesa stimata di {{period}}.", "acknowledgeSpendError": "Confermare la spesa stimata di {{period}}.",
"acknowledgeSpendErrorMonthly": "Confermare la spesa mensile stimata.", "acknowledgeSpendErrorMonthly": "Confermare la spesa mensile stimata.",
"acknowledgeSpendErrorDaily": "Confermare la spesa giornaliera stimata." "acknowledgeSpendErrorDaily": "Confermare la spesa giornaliera stimata."
@@ -469,7 +471,7 @@
"sharded": "Partizionato", "sharded": "Partizionato",
"addPartitionKey": "Aggiungi chiave di partizione gerarchica", "addPartitionKey": "Aggiungi chiave di partizione gerarchica",
"hierarchicalPartitionKeyInfo": "Questa funzionalità consente di partizionare i dati con fino a tre livelli di chiavi per una migliore distribuzione dei dati. Richiede .NET V3, Java V4 SDK o JavaScript V3 SDK in anteprima.", "hierarchicalPartitionKeyInfo": "Questa funzionalità consente di partizionare i dati con fino a tre livelli di chiavi per una migliore distribuzione dei dati. Richiede .NET V3, Java V4 SDK o JavaScript V3 SDK in anteprima.",
"provisionDedicatedThroughput": "Eseguire il provisioning della velocità effettiva dedicata per {{collectionName}}", "provisionDedicatedThroughput": "Esegui il provisioning della velocità effettiva dedicata per {{collectionName}}",
"provisionDedicatedThroughputTooltip": "È possibile effettuare facoltativamente il provisioning di una velocità effettiva dedicata per un {{collectionName}} di un database che ha già una velocità effettiva con provisioning. Questa velocità effettiva dedicata non sarà condivisa con altri {{collectionNamePlural}} nel database e non verrà conteggiata nella velocità effettiva con provisioning per il database. Questa velocità effettiva verrà fatturata in aggiunta a quella con provisioning a livello di database.", "provisionDedicatedThroughputTooltip": "È possibile effettuare facoltativamente il provisioning di una velocità effettiva dedicata per un {{collectionName}} di un database che ha già una velocità effettiva con provisioning. Questa velocità effettiva dedicata non sarà condivisa con altri {{collectionNamePlural}} nel database e non verrà conteggiata nella velocità effettiva con provisioning per il database. Questa velocità effettiva verrà fatturata in aggiunta a quella con provisioning a livello di database.",
"uniqueKeysPlaceholderMongo": "Percorsi separati da virgole, ad esempio, firstName,address.zipCode", "uniqueKeysPlaceholderMongo": "Percorsi separati da virgole, ad esempio, firstName,address.zipCode",
"uniqueKeysPlaceholderSql": "Percorsi separati da virgole, ad esempio /firstName,/address/zipCode", "uniqueKeysPlaceholderSql": "Percorsi separati da virgole, ad esempio /firstName,/address/zipCode",
@@ -492,7 +494,7 @@
"acknowledgeSpendErrorMonthly": "Confermare la spesa mensile stimata.", "acknowledgeSpendErrorMonthly": "Confermare la spesa mensile stimata.",
"acknowledgeSpendErrorDaily": "Confermare la spesa giornaliera stimata.", "acknowledgeSpendErrorDaily": "Confermare la spesa giornaliera stimata.",
"unshardedMaxRuError": "Le raccolte non partizionate supportano fino a 10.000 UR", "unshardedMaxRuError": "Le raccolte non partizionate supportano fino a 10.000 UR",
"acknowledgeShareThroughputError": "Confermare il costo stimato di questa velocità effettiva dedicata.", "acknowledgeShareThroughputError": "Conferma il costo stimato di questa velocità effettiva dedicata.",
"vectorPolicyError": "Correggere gli errori nei criteri sul vettore contenitore", "vectorPolicyError": "Correggere gli errori nei criteri sul vettore contenitore",
"fullTextSearchPolicyError": "Correggere gli errori nei criteri di ricerca full-text del contenitore", "fullTextSearchPolicyError": "Correggere gli errori nei criteri di ricerca full-text del contenitore",
"addingSampleDataSet": "Aggiunta dei set di dati di esempio", "addingSampleDataSet": "Aggiunta dei set di dati di esempio",
@@ -708,7 +710,7 @@
"tableIdLabel": "Immettere il comando CQL per creare la tabella.", "tableIdLabel": "Immettere il comando CQL per creare la tabella.",
"enterTableId": "Immetti ID tabella", "enterTableId": "Immetti ID tabella",
"tableSchemaAriaLabel": "Schema della tabella", "tableSchemaAriaLabel": "Schema della tabella",
"provisionDedicatedThroughput": "Eseguire il provisioning della velocità effettiva dedicata per questa tabella", "provisionDedicatedThroughput": "Esegui il provisioning della velocità effettiva dedicata per questa tabella",
"provisionDedicatedThroughputTooltip": "È possibile effettuare facoltativamente il provisioning di una capacità effettiva dedicata per una tabella all'interno di un keyspace che ha già una capacità con provisioning. Questa capacità effettiva dedicata non sarà condivisa con altre tabelle nel keyspace e non verrà conteggiata nella capacità con provisioning per il keyspace. Questa capacità effettiva verrà fatturata in aggiunta a quella con provisioning a livello di keyspace." "provisionDedicatedThroughputTooltip": "È possibile effettuare facoltativamente il provisioning di una capacità effettiva dedicata per una tabella all'interno di un keyspace che ha già una capacità con provisioning. Questa capacità effettiva dedicata non sarà condivisa con altre tabelle nel keyspace e non verrà conteggiata nella capacità con provisioning per il keyspace. Questa capacità effettiva verrà fatturata in aggiunta a quella con provisioning a livello di keyspace."
}, },
"tables": { "tables": {
@@ -762,7 +764,7 @@
"computedProperties": "Proprietà calcolate", "computedProperties": "Proprietà calcolate",
"containerPolicies": "Criteri contenitore", "containerPolicies": "Criteri contenitore",
"throughputBuckets": "Bucket di velocità effettiva", "throughputBuckets": "Bucket di velocità effettiva",
"globalSecondaryIndexPreview": "Indice secondario globale (anteprima)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "Criteri di maschera" "maskingPolicyPreview": "Criteri di maschera"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -795,7 +797,7 @@
"perMonth": "/mese" "perMonth": "/mese"
}, },
"throughput": { "throughput": {
"manualToAutoscaleDisclaimer": "Il numero massimo di UR/sec con scalabilità automatica iniziale verrà determinato dal sistema, in base alle impostazioni correnti della velocità effettiva manuale e all'archiviazione della risorsa. Dopo aver abilitato la scalabilità automatica, è possibile modificare il numero massimo di UR/sec.", "manualToAutoscaleDisclaimer": "Il numero massimo di UR/s con scalabilità automatica iniziale verrà determinato dal sistema, in base alle impostazioni correnti della velocità effettiva configurata manualmente e all'archiviazione della risorsa. Dopo aver abilitato la scalabilità automatica, è possibile modificare il numero massimo di UR/s.",
"ttlWarningText": "Il sistema eliminerà automaticamente gli elementi in base al valore TTL (in secondi) specificato, senza la necessità di un'operazione di eliminazione eseguita in modo esplicito da un'applicazione client. Per altre informazioni, vedere", "ttlWarningText": "Il sistema eliminerà automaticamente gli elementi in base al valore TTL (in secondi) specificato, senza la necessità di un'operazione di eliminazione eseguita in modo esplicito da un'applicazione client. Per altre informazioni, vedere",
"ttlWarningLinkText": "Durata (TTL) in Azure Cosmos DB", "ttlWarningLinkText": "Durata (TTL) in Azure Cosmos DB",
"unsavedIndexingPolicy": "criteri di indicizzazione", "unsavedIndexingPolicy": "criteri di indicizzazione",
@@ -803,24 +805,24 @@
"unsavedComputedProperties": "proprietà calcolate", "unsavedComputedProperties": "proprietà calcolate",
"unsavedEditorWarningPrefix": "Non sono state salvate le modifiche più recenti apportate a", "unsavedEditorWarningPrefix": "Non sono state salvate le modifiche più recenti apportate a",
"unsavedEditorWarningSuffix": ". Fare clic su Salva per confermare le modifiche.", "unsavedEditorWarningSuffix": ". Fare clic su Salva per confermare le modifiche.",
"updateDelayedApplyWarning": "Si sta per richiedere un aumento della velocità effettiva oltre la capacità preallocata. Il completamento dell'operazione potrebbe richiedere alcuni minuti.", "updateDelayedApplyWarning": "Stai per richiedere un aumento della velocità effettiva oltre la capacità preallocata. Il completamento dell'operazione potrebbe richiedere alcuni minuti.",
"scalingUpDelayMessage": "L'aumento richiederà 4-6 ore perché supera quello che Azure Cosmos DB può attualmente supportare immediatamente in base al numero di partizioni fisiche. È possibile aumentare immediatamente la velocità effettiva a {{instantMaximumThroughput}} o procedere con questo valore e attendere il completamento dell'aumento.", "scalingUpDelayMessage": "L'aumento richiederà 4-6 ore perché supera quello che Azure Cosmos DB può attualmente supportare in base al numero di partizioni fisiche. È possibile aumentare immediatamente la velocità effettiva a {{instantMaximumThroughput}} o procedere con questo valore e attendere il completamento dell'aumento.",
"exceedPreAllocatedMessage": "La richiesta di aumento della velocità effettiva supera la capacità preallocata, quindi potrebbe richiedere più tempo del previsto. Per continuare, è possibile scegliere tra tre opzioni:", "exceedPreAllocatedMessage": "La richiesta di aumento della velocità effettiva supera la capacità preallocata, quindi potrebbe richiedere più tempo del previsto. Per continuare, puoi scegliere tra tre opzioni:",
"instantScaleOption": "È possibile aumentare immediatamente fino a {{instantMaximumThroughput}} UR/sec.", "instantScaleOption": "È possibile aumentare immediatamente fino a {{instantMaximumThroughput}} UR/s.",
"asyncScaleOption": "È possibile aumentare in modo asincrono fino a qualsiasi valore inferiore a {{maximumThroughput}} UR/sec in 4-6 ore.", "asyncScaleOption": "È possibile aumentare in modo asincrono fino a qualsiasi valore inferiore a {{maximumThroughput}} UR/s in 4-6 ore.",
"quotaMaxOption": "Il valore massimo della quota corrente è {{maximumThroughput}} UR/s. Per superare questo limite, è necessario richiedere un aumento della quota e il team di Azure Cosmos DB esaminerà la richiesta.", "quotaMaxOption": "Il valore massimo della quota corrente è {{maximumThroughput}} UR/s. Per superare questo limite, è necessario richiedere un aumento della quota e il team di Azure Cosmos DB esaminerà la richiesta.",
"belowMinimumMessage": "Non è possibile ridurre la velocità effettiva al di sotto del valore minimo corrente di {{minimum}} UR/sec. Per altre informazioni su questo limite, fare riferimento alla documentazione relativa alle quote dei servizi.", "belowMinimumMessage": "Non è possibile ridurre la velocità effettiva al di sotto del valore minimo corrente di {{minimum}} UR/s. Per altre informazioni su questo limite, fai riferimento alla documentazione relativa alle quote dei servizi.",
"saveThroughputWarning": "La fattura sarà interessata dall'aggiornamento delle impostazioni della velocità effettiva. Prima di salvare le modifiche, esaminare la stima dei costi aggiornata riportata di seguito", "saveThroughputWarning": "La fattura sarà interessata dall'aggiornamento delle impostazioni della velocità effettiva. Prima di salvare le modifiche, esamina la stima dei costi aggiornata riportata di seguito",
"currentAutoscaleThroughput": "Velocità effettiva con scalabilità automatica corrente:", "currentAutoscaleThroughput": "Velocità effettiva con scalabilità automatica corrente:",
"targetAutoscaleThroughput": "Velocità effettiva con scalabilità automatica di destinazione:", "targetAutoscaleThroughput": "Velocità effettiva con scalabilità automatica di destinazione:",
"currentManualThroughput": "Velocità effettiva manuale corrente:", "currentManualThroughput": "Velocità effettiva corrente configurata manualmente:",
"targetManualThroughput": "Velocità effettiva manuale di destinazione:", "targetManualThroughput": "Velocità effettiva di destinazione configurata manualmente:",
"applyDelayedMessage": "La richiesta di aumento della velocità effettiva è stata inviata. Il completamento dell'operazione richiederà da 1 a 3 giorni lavorativi. Visualizzare lo stato più recente nelle notifiche.", "applyDelayedMessage": "La richiesta di aumento della velocità effettiva è stata inviata. Il completamento dell'operazione richiederà da 1 a 3 giorni lavorativi. Visualizza lo stato più recente nelle notifiche.",
"databaseLabel": "Database:", "databaseLabel": "Database:",
"containerLabel": "Contenitore:", "containerLabel": "Contenitore:",
"applyShortDelayMessage": "È attualmente in corso una richiesta di aumento della velocità effettiva. Il completamento dell'operazione potrebbe richiedere alcuni minuti.", "applyShortDelayMessage": "È attualmente in corso una richiesta di aumento della velocità effettiva. Il completamento dell'operazione potrebbe richiedere alcuni minuti.",
"applyLongDelayMessage": "È attualmente in corso una richiesta di aumento della velocità effettiva. Il completamento dell'operazione richiederà da 1 a 3 giorni lavorativi. Visualizzare lo stato più recente nelle notifiche.", "applyLongDelayMessage": "È attualmente in corso una richiesta di aumento della velocità effettiva. Il completamento dell'operazione richiederà da 1 a 3 giorni lavorativi. Visualizza lo stato più recente nelle notifiche.",
"throughputCapError": "L'account è attualmente configurato con un limite di velocità effettiva totale di {{throughputCap}} UR/sec. Questo aggiornamento non è possibile perché aumenterebbe la velocità effettiva totale fino a {{newTotalThroughput}} UR/sec. Modificare il limite di velocità effettiva totale nella gestione dei costi.", "throughputCapError": "L'account è attualmente configurato con un limite di velocità effettiva totale di {{throughputCap}} UR/s. Questo aggiornamento non è possibile perché aumenterebbe la velocità effettiva totale fino a {{newTotalThroughput}} UR/s. Modifica il limite di velocità effettiva totale nella gestione dei costi.",
"throughputIncrementError": "Il valore della velocità effettiva deve essere in incrementi di 1000" "throughputIncrementError": "Il valore della velocità effettiva deve essere in incrementi di 1000"
}, },
"conflictResolution": { "conflictResolution": {
@@ -841,9 +843,9 @@
"mongoIndexing": { "mongoIndexing": {
"disclaimer": "Per le query che filtrano in base a più proprietà, creare più indici di campi singoli anziché un indice composto.", "disclaimer": "Per le query che filtrano in base a più proprietà, creare più indici di campi singoli anziché un indice composto.",
"disclaimerCompoundIndexesLink": " Indici composti ", "disclaimerCompoundIndexesLink": " Indici composti ",
"disclaimerSuffix": "vengono usati solo per l'ordinamento dei risultati della query. Se è necessario aggiungere un indice composto, è possibile crearne uno usando la shell Mongo.", "disclaimerSuffix": "vengono usati solo per l'ordinamento dei risultati della query. Se è necessario aggiungere un indice composto, è possibile crearne uno usando Mongo Shell.",
"compoundNotSupported": "Le raccolte con indici composti non sono ancora supportate nell'editor di indicizzazione. Per modificare i criteri di indicizzazione per questa raccolta, usare la shell Mongo.", "compoundNotSupported": "Le raccolte con indici composti non sono ancora supportate nell'editor di indicizzazione. Per modificare i criteri di indicizzazione per questa raccolta, usa Mongo Shell.",
"aadError": "Per usare l'editor dei criteri di indicizzazione, accedere a", "aadError": "Per usare l'editor dei criteri di indicizzazione, accedi a",
"aadErrorLink": "portale di Azure.", "aadErrorLink": "portale di Azure.",
"refreshingProgress": "Aggiornamento dello stato di avanzamento della trasformazione dell'indice", "refreshingProgress": "Aggiornamento dello stato di avanzamento della trasformazione dell'indice",
"canMakeMoreChangesZero": "Al termine della trasformazione dell'indice corrente, è possibile apportare altre modifiche all'indicizzazione. ", "canMakeMoreChangesZero": "Al termine della trasformazione dell'indice corrente, è possibile apportare altre modifiche all'indicizzazione. ",
@@ -885,10 +887,10 @@
"scale": { "scale": {
"freeTierInfo": "Con il livello gratuito si otterranno gratuitamente le prime {{ru}} UR/s e {{storage}} GB di spazio di archiviazione in questo account. Per mantenere l'account gratuito, mantenere il totale delle UR/sec in tutte le risorse dell'account al di sotto di {{ru}} UR/sec.", "freeTierInfo": "Con il livello gratuito si otterranno gratuitamente le prime {{ru}} UR/s e {{storage}} GB di spazio di archiviazione in questo account. Per mantenere l'account gratuito, mantenere il totale delle UR/sec in tutte le risorse dell'account al di sotto di {{ru}} UR/sec.",
"freeTierLearnMore": "Altre informazioni.", "freeTierLearnMore": "Altre informazioni.",
"throughputRuS": "Unità elaborate (UR/sec)", "throughputRuS": "Velocità effettiva (UR/s)",
"autoScaleCustomSettings": "L'account ha impostazioni personalizzate che impediscono l'impostazione della velocità effettiva a livello di contenitore. Collaborare con il punto di contatto del team tecnico di Cosmos DB per apportare modifiche.", "autoScaleCustomSettings": "L'account ha impostazioni personalizzate che impediscono la configurazione della velocità effettiva a livello di contenitore. Collabora con il punto di contatto del team tecnico di Cosmos DB per apportare modifiche.",
"keyspaceSharedThroughput": "La capacità effettiva condivisa di questa tabella è configurata nel keyspace", "keyspaceSharedThroughput": "La capacità effettiva condivisa di questa tabella è configurata nel keyspace",
"throughputRangeLabel": "Velocità effettiva ({{min}}-{{max}} UR/s)", "throughputRangeLabel": "Velocità effettiva ({{min}} - {{max}} UR/s)",
"unlimited": "senza limiti" "unlimited": "senza limiti"
}, },
"partitionKeyEditor": { "partitionKeyEditor": {
@@ -928,7 +930,7 @@
"learnMoreSuffix": "su come definire gli indici secondari globali e su come usarli.", "learnMoreSuffix": "su come definire gli indici secondari globali e su come usarli.",
"jsonAriaLabel": "JSON indice secondario globale", "jsonAriaLabel": "JSON indice secondario globale",
"addIndex": "Aggiungi indice", "addIndex": "Aggiungi indice",
"settingsTitle": "Impostazioni globali dell'indice secondario", "settingsTitle": "Impostazioni dell'indice secondario globale",
"sourceContainer": "Contenitore di origine", "sourceContainer": "Contenitore di origine",
"indexDefinition": "Definizione di indice secondario globale" "indexDefinition": "Definizione di indice secondario globale"
}, },
@@ -947,7 +949,7 @@
"instant": "Immediata", "instant": "Immediata",
"fourToSixHrs": "4-6 ore", "fourToSixHrs": "4-6 ore",
"autoscaleDescription": "In base all'utilizzo, la velocità effettiva {{resourceType}} verrà ridimensionata da", "autoscaleDescription": "In base all'utilizzo, la velocità effettiva {{resourceType}} verrà ridimensionata da",
"freeTierWarning": "La fatturazione verrà applicata se si effettua il provisioning di più di {{ru}} UR/sec di velocità effettiva manuale o se la risorsa viene ridimensionata oltre a {{ru}} UR/sec con scalabilità automatica.", "freeTierWarning": "La fatturazione verrà applicata se si effettua il provisioning di più di {{ru}} UR/s di velocità effettiva configurata manualmente o se la risorsa viene ridimensionata oltre a {{ru}} UR/s con scalabilità automatica.",
"capacityCalculator": "Stima le UR/sec necessarie con", "capacityCalculator": "Stima le UR/sec necessarie con",
"capacityCalculatorLink": " calcolatore capacità", "capacityCalculatorLink": " calcolatore capacità",
"fixedStorageNote": "Quando si usa una raccolta con capacità di archiviazione fissa, è possibile impostare fino a 10.000 UR/sec.", "fixedStorageNote": "Quando si usa una raccolta con capacità di archiviazione fissa, è possibile impostare fino a 10.000 UR/sec.",
@@ -961,7 +963,7 @@
"active": "Attivo", "active": "Attivo",
"inactive": "Inattivo", "inactive": "Inattivo",
"defaultBucketLabel": "Contenitore di velocità effettiva predefinito", "defaultBucketLabel": "Contenitore di velocità effettiva predefinito",
"defaultBucketPlaceholder": "Selezionare un contenitore di velocità effettiva predefinito", "defaultBucketPlaceholder": "Seleziona un contenitore di velocità effettiva predefinito",
"defaultBucketTooltip": "Il contenitore di velocità effettiva predefinito viene usato per le operazioni che non specificano un contenitore particolare.", "defaultBucketTooltip": "Il contenitore di velocità effettiva predefinito viene usato per le operazioni che non specificano un contenitore particolare.",
"defaultBucketTooltipLearnMore": "Altre informazioni.", "defaultBucketTooltipLearnMore": "Altre informazioni.",
"noDefaultBucketSelected": "Nessun contenitore di velocità effettiva predefinito selezionato", "noDefaultBucketSelected": "Nessun contenitore di velocità effettiva predefinito selezionato",
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "La dimensione di quantizzazione in byte deve essere maggiore di 0 e minore o uguale a 512", "quantizationByteSizeRangeError": "La dimensione di quantizzazione in byte deve essere maggiore di 0 e minore o uguale a 512",
"indexingSearchListSizeRangeError": "La dimensione dell'elenco di ricerca di indicizzazione deve essere maggiore o uguale a 25 e minore o uguale a 500" "indexingSearchListSizeRangeError": "La dimensione dell'elenco di ricerca di indicizzazione deve essere maggiore o uguale a 25 e minore o uguale a 500"
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "Feedback",
"feedbackButtonAriaLabel": "Fornisci feedback sui processi di copia",
"refreshButtonAriaLabel": "Aggiorna processi di copia",
"createCopyJobButtonLabel": "Crea processo di copia",
"createCopyJobButtonAriaLabel": "Crea un nuovo processo di copia del contenitore"
},
"noCopyJobs": {
"title": "Nessun processo di copia da visualizzare",
"createCopyJobButtonText": "Crea un processo di copia del contenitore"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "Dettagli processo",
"errorTitle": "Dettagli errore",
"selectedContainers": "Contenitori selezionati"
},
"createCopyJob": {
"panelTitle": "Crea processo di copia"
},
"selectAccount": {
"description": "Selezionare un account di destinazione in cui copiare.",
"subscriptionDropdownLabel": "Sottoscrizione",
"subscriptionDropdownPlaceholder": "Selezionare una sottoscrizione",
"accountDropdownLabel": "Account",
"accountDropdownPlaceholder": "Selezionare un account"
},
"migrationType": {
"offline": {
"title": "Modalità offline",
"description": "I processi di copia dei contenitori offline consentono di copiare i dati da un contenitore di origine a un contenitore di destinazione di Cosmos DB per le API supportate. Per garantire l'integrità dei dati tra origine e destinazione, è consigliabile arrestare gli aggiornamenti nel contenitore di origine prima di creare il processo di copia. Altre informazioni sui [processi di copia offline](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql)."
},
"online": {
"title": "Modalità online",
"description": "I processi di copia dei contenitori online consentono di copiare i dati da un contenitore di origine a un contenitore di destinazione dell'API NoSQL di Cosmos DB usando il feed di modifiche [Tutte le versioni ed eliminazioni](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). In questo modo gli aggiornamenti possono continuare nell'origine durante la copia dei dati. Alla fine è necessario un breve periodo di inattività per passare in modo sicuro le applicazioni client al contenitore di destinazione. Altre informazioni sui [processi di copia online](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
"selectContainers": {
"description": "Selezionare un contenitore di origine e un contenitore di destinazione in cui copiare.",
"sourceContainerSubHeading": "Contenitore di origine",
"targetContainerSubHeading": "Contenitore di destinazione",
"databaseDropdownLabel": "Database",
"databaseDropdownPlaceholder": "Selezionare un database",
"containerDropdownLabel": "Contenitore",
"containerDropdownPlaceholder": "Selezionare un contenitore",
"createNewContainerSubHeading": "Configurare le proprietà del nuovo contenitore nell'account di destinazione \"{{accountName}}\".",
"createNewContainerSubHeadingDefault": "Configurare le proprietà per il nuovo contenitore.",
"createContainerButtonLabel": "Crea un nuovo contenitore",
"createContainerHeading": "Crea nuovo contenitore"
},
"preview": {
"jobNameLabel": "Nome processo",
"subscriptionLabel": "Sottoscrizione di destinazione",
"accountLabel": "Account di destinazione",
"sourceDatabaseLabel": "Database di origine",
"sourceContainerLabel": "Contenitore di origine",
"targetDatabaseLabel": "Database di destinazione",
"targetContainerLabel": "Contenitore di destinazione"
},
"assignPermissions": {
"crossAccountDescription": "Per copiare i dati dal contenitore di origine a quello di destinazione, assicurarsi che l'identità gestita dell'account di origine disponga dell'accesso in lettura/scrittura all'account di destinazione completando la procedura seguente.",
"intraAccountOnlineDescription": "Seguire questa procedura per abilitare la copia online nell'account \"{{accountName}}\".",
"crossAccountConfiguration": {
"title": "Copia contenitore tra account",
"description": "Seguire le istruzioni indicate di seguito per concedere le autorizzazioni necessarie a copiare i dati da \"{{sourceAccount}}\" a \"{{destinationAccount}}\"."
},
"onlineConfiguration": {
"title": "Copia contenitore online",
"description": "Seguire le istruzioni seguenti per abilitare la copia online nell'account \"{{accountName}}\"."
}
},
"popoverOverlaySpinnerLabel": "Attendere il completamento dell'elaborazione della richiesta...",
"addManagedIdentity": {
"title": "Identità gestita assegnata dal sistema abilitata.",
"description": "Un'identità gestita assegnata dal sistema è limitata a una per risorsa ed è legata al ciclo di vita di tale risorsa. Dopo l'abilitazione, è possibile concedere le autorizzazioni all'identità gestita usando il controllo degli accessi in base al ruolo di Azure (RBAC di Azure). L'identità gestita viene autenticata con Microsoft Entra ID, quindi non è necessario archiviare credenziali nel codice.",
"descriptionHrefText": "Altre informazioni sulle identità gestite.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "Identità gestita assegnata dal sistema",
"tooltipContent": "Altre informazioni",
"tooltipHrefText": "Identità gestite.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "È possibile selezionare un'identità assegnata dall'utente esistente o crearne una nuova.",
"userAssignedIdentityLabel": "È anche possibile selezionare un'identità gestita assegnata dall'utente.",
"createUserAssignedIdentityLink": "Crea identità gestita assegnata dall'utente",
"enablementTitle": "Abilitare l'identità gestita assegnata dal sistema",
"enablementDescription": "Abilitare l'identità gestita assegnata dal sistema nel {{accountName}}. Per confermare, fare clic sul pulsante \"Sì\"."
},
"defaultManagedIdentity": {
"title": "Identità gestita assegnata dal sistema impostata come predefinita.",
"description": "Impostare l'identità gestita assegnata dal sistema come predefinita per \"{{accountName}}\" attivandola.",
"tooltipContent": "Altre informazioni",
"tooltipHrefText": "Identità gestite predefinite.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "Identità gestita assegnata dal sistema impostata come predefinita",
"popoverDescription": "Assegnare l'identità gestita assegnata dal sistema come predefinita per \"{{accountName}}\". Per confermare, fare clic sul pulsante \"Sì\". "
},
"readWritePermissionAssigned": {
"title": "Autorizzazioni di lettura/scrittura assegnate all'identità predefinita.",
"description": "Per consentire la copia dei dati dal contenitore di origine a quello di destinazione, concedere all'identità predefinita dell'account di origine l'accesso in lettura/scrittura dell'account di destinazione.",
"tooltipContent": "Altre informazioni",
"tooltipHrefText": "Autorizzazioni di lettura/scrittura.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "Assegnare autorizzazioni di lettura/scrittura all'identità predefinita.",
"popoverDescription": "Assegnare all'identità predefinita dell'account di origine le autorizzazioni di lettura/scrittura dell'account di destinazione. Per confermare, fare clic sul pulsante \"Sì\"."
},
"pointInTimeRestore": {
"title": "Ripristino temporizzato abilitato",
"description": "Per agevolare i processi di copia online dei contenitori, aggiorna il criterio di backup \"{{accessName}}\" da backup periodico a backup continuo. Per questa funzionalità è necessario abilitare il backup continuo.",
"tooltipContent": "Altre informazioni",
"tooltipHrefText": "Backup continuo",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "Abilita ripristino temporizzato"
},
"onlineCopyEnabled": {
"title": "Copia online abilitata",
"description": "Abilitare la copia online del contenitore facendo clic sul pulsante seguente nell'account \"{{accountName}}\".",
"hrefText": "Altre informazioni sui processi di copia online",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "Abilita copia online",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Convalida della modalità feed di modifiche per tutte le versioni ed eliminazioni (anteprima)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Abilitazione della modalità feed di modifiche per tutte le versioni ed eliminazioni (anteprima)...",
"enablingOnlineCopySpinnerLabel": "Abilitazione della copia online nell'account \"{{accountName}}\"..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "Data e ora",
"name": "Nome processo",
"status": "Stato",
"completionPercentage": "% completamento",
"duration": "Durata",
"error": "Messaggio di errore",
"mode": "Modalità",
"actions": "Azioni"
},
"actions": {
"pause": "Sospendi",
"resume": "Riprendi",
"complete": "Completamento",
"viewDetails": "Visualizza dettagli"
},
"status": {
"pending": "In coda",
"inProgress": "In esecuzione",
"running": "In esecuzione",
"partitioning": "In esecuzione",
"paused": "In pausa",
"completed": "Completato",
"failed": "Non riuscito",
"faulted": "Non riuscito",
"skipped": "Annullato",
"cancelled": "Annullato"
},
"dialog": {
"confirmButtonText": "Conferma",
"cancelButtonText": "Annulla"
}
}
} }
} }
+167 -6
View File
@@ -34,6 +34,8 @@
"browse": "参照", "browse": "参照",
"increaseValueBy1": "値を 1 増加", "increaseValueBy1": "値を 1 増加",
"decreaseValueBy1": "値を 1 減少", "decreaseValueBy1": "値を 1 減少",
"on": "オン",
"off": "オフ",
"preview": "プレビュー" "preview": "プレビュー"
}, },
"splashScreen": { "splashScreen": {
@@ -76,7 +78,7 @@
"description": "PostgreSQL のシェル インターフェイスを使用して、テーブルを作成し、データを操作します" "description": "PostgreSQL のシェル インターフェイスを使用して、テーブルを作成し、データを操作します"
}, },
"vcoreMongo": { "vcoreMongo": {
"title": "Mongo シェル", "title": "Mongo Shell",
"description": "MongoDB のシェル インターフェイスを使用して、コレクションを作成し、データを操作します" "description": "MongoDB のシェル インターフェイスを使用して、コレクションを作成し、データを操作します"
} }
}, },
@@ -303,7 +305,7 @@
"deleteContainer": "{{containerName}} の削除", "deleteContainer": "{{containerName}} の削除",
"newSqlQuery": "新しい SQL クエリ", "newSqlQuery": "新しい SQL クエリ",
"newQuery": "新しいクエリ", "newQuery": "新しいクエリ",
"openMongoShell": "Mongo シェルを開く", "openMongoShell": "Mongo Shell を開く",
"newShell": "新しいシェル", "newShell": "新しいシェル",
"openCassandraShell": "Cassandra シェルを開く", "openCassandraShell": "Cassandra シェルを開く",
"newStoredProcedure": "新しいストアド プロシージャ", "newStoredProcedure": "新しいストアド プロシージャ",
@@ -414,7 +416,7 @@
"refreshGridFailed": "ドキュメント グリッドの更新に失敗しました" "refreshGridFailed": "ドキュメント グリッドの更新に失敗しました"
}, },
"mongoShell": { "mongoShell": {
"title": "Mongo シェル" "title": "Mongo Shell"
} }
}, },
"panes": { "panes": {
@@ -762,7 +764,7 @@
"computedProperties": "計算されたプロパティ", "computedProperties": "計算されたプロパティ",
"containerPolicies": "コンテナー ポリシー", "containerPolicies": "コンテナー ポリシー",
"throughputBuckets": "スループット バケット", "throughputBuckets": "スループット バケット",
"globalSecondaryIndexPreview": "グローバル セカンダリ インデックス (プレビュー)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "マスキング ポリシー" "maskingPolicyPreview": "マスキング ポリシー"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -841,8 +843,8 @@
"mongoIndexing": { "mongoIndexing": {
"disclaimer": "複数のプロパティでフィルター処理するクエリの場合は、複合インデックスではなく、複数の単一フィールド インデックスを作成してください。", "disclaimer": "複数のプロパティでフィルター処理するクエリの場合は、複合インデックスではなく、複数の単一フィールド インデックスを作成してください。",
"disclaimerCompoundIndexesLink": " 複合インデックス ", "disclaimerCompoundIndexesLink": " 複合インデックス ",
"disclaimerSuffix": "は、クエリ結果の並べ替えにのみ使用されます。複合インデックスを追加する必要がある場合は、Mongo シェルを使用して作成できます。", "disclaimerSuffix": "は、クエリ結果の並べ替えにのみ使用されます。複合インデックスを追加する必要がある場合は、Mongo Shell を使用して作成できます。",
"compoundNotSupported": "複合インデックスを持つコレクションは、インデックス作成エディターではまだサポートされていません。このコレクションのインデックス作成ポリシーを変更するには、Mongo シェルを使用してください。", "compoundNotSupported": "複合インデックスを持つコレクションは、インデックス作成エディターではまだサポートされていません。このコレクションのインデックス作成ポリシーを変更するには、Mongo Shell を使用してください。",
"aadError": "インデックス作成ポリシー エディターを使用するには、次にログインしてください:", "aadError": "インデックス作成ポリシー エディターを使用するには、次にログインしてください:",
"aadErrorLink": "Azure portal。", "aadErrorLink": "Azure portal。",
"refreshingProgress": "インデックス変換の進行状況を更新しています", "refreshingProgress": "インデックス変換の進行状況を更新しています",
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "量子化バイト サイズは 0 より大きく、512 以下である必要があります", "quantizationByteSizeRangeError": "量子化バイト サイズは 0 より大きく、512 以下である必要があります",
"indexingSearchListSizeRangeError": "インデックス検索リスト サイズは 25 以上 500 以下である必要があります" "indexingSearchListSizeRangeError": "インデックス検索リスト サイズは 25 以上 500 以下である必要があります"
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "フィードバック",
"feedbackButtonAriaLabel": "コピー ジョブに関するフィードバックを提供する",
"refreshButtonAriaLabel": "コピー ジョブの更新",
"createCopyJobButtonLabel": "コピー ジョブの作成",
"createCopyJobButtonAriaLabel": "新しいコンテナー コピー ジョブの作成"
},
"noCopyJobs": {
"title": "表示するコピー ジョブはありません",
"createCopyJobButtonText": "コンテナー コピー ジョブの作成"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "ジョブの詳細",
"errorTitle": "エラーの詳細",
"selectedContainers": "選択したコンテナー"
},
"createCopyJob": {
"panelTitle": "コピー ジョブの作成"
},
"selectAccount": {
"description": "コピー先のアカウントを選択してください。",
"subscriptionDropdownLabel": "サブスクリプション",
"subscriptionDropdownPlaceholder": "サブスクリプションを選択します",
"accountDropdownLabel": "アカウント",
"accountDropdownPlaceholder": "アカウントの選択"
},
"migrationType": {
"offline": {
"title": "オフライン モード",
"description": "オフライン コンテナー コピー ジョブでは、サポートされている API のソース コンテナーからコピー先の Cosmos DB コンテナーにデータをコピーできます。ソースとコピー先の間でデータ整合性を確保するため、コピー ジョブを作成する前にソース コンテナーでの更新を停止することをお勧めします。[オフライン コピー ジョブ](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql) の詳細をご覧ください。"
},
"online": {
"title": "オンライン モード",
"description": "オンライン コンテナー コピー ジョブでは、[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) 変更フィードを使用して、ソース コンテナーからコピー先の Cosmos DB NoSQL API コンテナーにデータをコピーできます。これにより、データのコピー中もソースで更新を継続できます。クライアント アプリケーションをコピー先コンテナーに安全に切り替えるには、最後に短時間のダウンタイムが必要です。[online copy jobs](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started) についての詳細をご覧ください。"
}
},
"selectContainers": {
"description": "コピー元のコンテナーとコピー先のコンテナーを選択してください。",
"sourceContainerSubHeading": "ソース コンテナー",
"targetContainerSubHeading": "コピー先のコンテナー",
"databaseDropdownLabel": "データベース",
"databaseDropdownPlaceholder": "データベースの選択",
"containerDropdownLabel": "コンテナー",
"containerDropdownPlaceholder": "コンテナーの選択",
"createNewContainerSubHeading": "コピー先のアカウント \"{{accountName}}\" の新しいコンテナーのプロパティを構成します。",
"createNewContainerSubHeadingDefault": "新しいコンテナーのプロパティを構成します。",
"createContainerButtonLabel": "新しいコンテナーを作成する",
"createContainerHeading": "新しいコンテナーの作成"
},
"preview": {
"jobNameLabel": "ジョブ名",
"subscriptionLabel": "変換先サービス",
"accountLabel": "コピー先のアカウント",
"sourceDatabaseLabel": "ソース データベース",
"sourceContainerLabel": "ソース コンテナー",
"targetDatabaseLabel": "転送先データベース:",
"targetContainerLabel": "コピー先のコンテナー"
},
"assignPermissions": {
"crossAccountDescription": "ソースからコピー先のコンテナーにデータをコピーするには、次の手順を実行して、ソース アカウントのマネージド ID にコピー先のアカウントへの読み取り/書き込みアクセス許可があることを確認します。",
"intraAccountOnlineDescription": "\"{{accountName}}\" アカウントでオンライン コピーを有効にするには、次の手順に従ってください。",
"crossAccountConfiguration": {
"title": "アカウント間でのコンテナーのコピー",
"description": "\"{{sourceAccount}}\" から \"{{destinationAccount}}\" にデータをコピーするために必要なアクセス許可を付与するには、以下の手順に従ってください。"
},
"onlineConfiguration": {
"title": "オンライン コンテナー コピー",
"description": "\"{{accountName}}\" アカウントでオンライン コピーを有効にするには、以下の手順に従ってください。"
}
},
"popoverOverlaySpinnerLabel": "要求を処理しています。しばらくお待ちください...",
"addManagedIdentity": {
"title": "システム割り当てマネージド ID が有効化されました。",
"description": "システム割り当てマネージド ID は 1 つのリソースにつき 1 つに限定されており、このリソースのライフサイクルに関連付けられています。有効にすると、Azure ロールベースのアクセス制御 (Azure RBAC) を使用して、マネージド ID にアクセス許可を付与できます。マネージド ID は Microsoft Entra ID で認証されるため、コード内に資格情報を格納する必要はありません。",
"descriptionHrefText": "マネージド ID に関する詳細情報。",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "システム割り当てマネージド ID",
"tooltipContent": "次に関する詳細をご確認ください:",
"tooltipHrefText": "マネージド ID。",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "既存のユーザー割り当てマネージド ID を選択するか、新しい ID を作成できます。",
"userAssignedIdentityLabel": "ユーザー割り当てマネージド ID を選択することもできます。",
"createUserAssignedIdentityLink": "ユーザー割り当てマネージド ID の作成",
"enablementTitle": "システム割り当てマネージド ID の有効化",
"enablementDescription": "{{accountName}} でシステム割り当てマネージド ID を有効にします。確認するには、[はい] ボタンをクリックします。"
},
"defaultManagedIdentity": {
"title": "システム割り当てマネージド ID は既定として設定されます。",
"description": "オンに切り替えて、システム割り当てマネージド ID を \"{{accountName}}\" の既定として設定します。",
"tooltipContent": "次に関する詳細をご確認ください:",
"tooltipHrefText": "既定のマネージド ID。",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "システム割り当てマネージド ID を既定として設定する",
"popoverDescription": "システム割り当てマネージド ID を \"{{accountName}}\" の既定として割り当てます。確認するには、[はい] ボタンをクリックします。"
},
"readWritePermissionAssigned": {
"title": "既定の ID に割り当てられた読み取り/書き込みアクセス許可。",
"description": "ソースからコピー先のコンテナーへのデータのコピーを許可するには、コピー先のアカウントでソース アカウントの既定の ID に読み取り/書き込みアクセス許可を付与します。",
"tooltipContent": "次に関する詳細をご確認ください:",
"tooltipHrefText": "読み取り/書き込みアクセス許可。",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "既定の ID に読み取り/書き込みアクセス許可を割り当てます。",
"popoverDescription": "コピー先アカウントの読み取り/書き込みアクセス許可を、ソース アカウントの既定の ID に割り当てます。確認するには、[はい] ボタンをクリックします。"
},
"pointInTimeRestore": {
"title": "ポイントインタイム リストアが有効です",
"description": "オンライン コンテナー コピー ジョブを有効にするには、\"{{accessName}}\" バックアップ ポリシーを定期バックアップから継続的バックアップに更新してください。この機能を使用するには、継続的バックアップを有効にする必要があります。",
"tooltipContent": "次に関する詳細をご確認ください:",
"tooltipHrefText": "継続的バックアップ",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "ポイントインタイム リストアを有効にする"
},
"onlineCopyEnabled": {
"title": "オンライン コピーが有効",
"description": "\"{{accountName}}\" アカウントで下のボタンをクリックして、オンライン コンテナー コピーを有効にします。",
"hrefText": "オンライン コピー ジョブの詳細情報",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "オンライン コピーを有効にする",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "すべてのバージョンを検証し、変更フィード モードを削除します (プレビュー)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "すべてのバージョンおよび削除の変更フィード モード (プレビュー) を有効にしています",
"enablingOnlineCopySpinnerLabel": "\"{{accountName}}\" アカウントでオンライン コピーを有効にしています..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "日付と時刻",
"name": "ジョブ名",
"status": "状態",
"completionPercentage": "完了率",
"duration": "期間",
"error": "エラー メッセージ",
"mode": "モード",
"actions": "アクション"
},
"actions": {
"pause": "一時停止",
"resume": "再開",
"complete": "完了",
"viewDetails": "詳細の表示"
},
"status": {
"pending": "キューに登録済み",
"inProgress": "実行中",
"running": "実行中",
"partitioning": "実行中",
"paused": "一時停止",
"completed": "完了済み",
"failed": "失敗",
"faulted": "失敗",
"skipped": "取り消し済み",
"cancelled": "取り消し済み"
},
"dialog": {
"confirmButtonText": "確認",
"cancelButtonText": "キャンセル"
}
}
} }
} }
+166 -5
View File
@@ -34,6 +34,8 @@
"browse": "찾아보기", "browse": "찾아보기",
"increaseValueBy1": "값을 1만큼 늘리기", "increaseValueBy1": "값을 1만큼 늘리기",
"decreaseValueBy1": "값을 1만큼 줄이기", "decreaseValueBy1": "값을 1만큼 줄이기",
"on": "켜기",
"off": "끄기",
"preview": "미리 보기" "preview": "미리 보기"
}, },
"splashScreen": { "splashScreen": {
@@ -619,7 +621,7 @@
"accountId": "계정 ID", "accountId": "계정 ID",
"sessionId": "세션 ID", "sessionId": "세션 ID",
"popupsDisabledError": "브라우저에서 팝업이 차단되어 이 계정에 대한 권한 부여를 설정할 수 없습니다.\n이 사이트에 대해 팝업을 허용한 후 \"Entra ID에 로그인\" 버튼을 클릭하세요.", "popupsDisabledError": "브라우저에서 팝업이 차단되어 이 계정에 대한 권한 부여를 설정할 수 없습니다.\n이 사이트에 대해 팝업을 허용한 후 \"Entra ID에 로그인\" 버튼을 클릭하세요.",
"failedToAcquireTokenError": "권한 부여 토큰을 자동으로 가져오지 못했습니다. Entra ID RBAC 작업을 사용하려면 \"Entra ID 로그인\" 버튼을 클릭하세요." "failedToAcquireTokenError": "인증 토큰을 자동으로 가져오지 못했습니다. Entra ID RBAC 작업을 활성화하려면 \"Entra ID 로그인\" 버튼을 클릭하세요."
}, },
"saveQuery": { "saveQuery": {
"panelTitle": "쿼리 저장", "panelTitle": "쿼리 저장",
@@ -762,7 +764,7 @@
"computedProperties": "계산된 속성", "computedProperties": "계산된 속성",
"containerPolicies": "컨테이너 정책", "containerPolicies": "컨테이너 정책",
"throughputBuckets": "처리량 버킷", "throughputBuckets": "처리량 버킷",
"globalSecondaryIndexPreview": "전역 보조 인덱스(미리 보기)", "globalSecondaryIndexPreview": "Global Secondary Index",
"maskingPolicyPreview": "마스킹 정책" "maskingPolicyPreview": "마스킹 정책"
}, },
"mongoNotifications": { "mongoNotifications": {
@@ -807,7 +809,7 @@
"scalingUpDelayMessage": "실제 파티션 수에 따라 현재 Azure Cosmos DB가 즉시 지원할 수 있는 한도를 초과하므로 확장에는 4~6시간이 소요됩니다. 처리량을 {{instantMaximumThroughput}}(으)로 즉시 늘리거나, 이 값을 유지하며 확장이 완료될 때까지 기다릴 수 있습니다.", "scalingUpDelayMessage": "실제 파티션 수에 따라 현재 Azure Cosmos DB가 즉시 지원할 수 있는 한도를 초과하므로 확장에는 4~6시간이 소요됩니다. 처리량을 {{instantMaximumThroughput}}(으)로 즉시 늘리거나, 이 값을 유지하며 확장이 완료될 때까지 기다릴 수 있습니다.",
"exceedPreAllocatedMessage": "처리량 증가 요청이 미리 할당된 용량을 초과하여 예상보다 오래 걸릴 수 있습니다. 계속하기 위해 선택할 수 있는 세 가지 옵션이 있습니다.", "exceedPreAllocatedMessage": "처리량 증가 요청이 미리 할당된 용량을 초과하여 예상보다 오래 걸릴 수 있습니다. 계속하기 위해 선택할 수 있는 세 가지 옵션이 있습니다.",
"instantScaleOption": "{{instantMaximumThroughput}}RU/s까지 즉시 확장할 수 있습니다.", "instantScaleOption": "{{instantMaximumThroughput}}RU/s까지 즉시 확장할 수 있습니다.",
"asyncScaleOption": "4~6시간 내에 {{maximumThroughput}}RU/s 이하의 값으로 비동기적으로 확장할 수 있습니다.", "asyncScaleOption": "4~6시간 내에 {{maximumThroughput}}RU/s 미만의 모든 값으로 비동기적으로 확장할 수 있습니다.",
"quotaMaxOption": "현재 할당량 최댓값은 {{maximumThroughput}}RU/s입니다. 이 한도를 초과하려면 할당량 증가를 요청해야 하며, Azure Cosmos DB 팀에서 검토합니다.", "quotaMaxOption": "현재 할당량 최댓값은 {{maximumThroughput}}RU/s입니다. 이 한도를 초과하려면 할당량 증가를 요청해야 하며, Azure Cosmos DB 팀에서 검토합니다.",
"belowMinimumMessage": "처리량을 현재 최소 {{minimum}}RU/s 미만으로 낮출 수 없습니다. 이 한도에 대한 자세한 내용은 서비스 견적 문서를 참고하세요.", "belowMinimumMessage": "처리량을 현재 최소 {{minimum}}RU/s 미만으로 낮출 수 없습니다. 이 한도에 대한 자세한 내용은 서비스 견적 문서를 참고하세요.",
"saveThroughputWarning": "처리량 설정을 변경하면 청구에 영향이 있습니다. 변경 내용을 저장하기 전에 아래에서 업데이트된 예상 비용을 검토하세요.", "saveThroughputWarning": "처리량 설정을 변경하면 청구에 영향이 있습니다. 변경 내용을 저장하기 전에 아래에서 업데이트된 예상 비용을 검토하세요.",
@@ -841,8 +843,8 @@
"mongoIndexing": { "mongoIndexing": {
"disclaimer": "여러 속성을 필터링하는 쿼리의 경우 복합 인덱스 대신 여러 개의 단일 필드 인덱스를 만드세요.", "disclaimer": "여러 속성을 필터링하는 쿼리의 경우 복합 인덱스 대신 여러 개의 단일 필드 인덱스를 만드세요.",
"disclaimerCompoundIndexesLink": " 복합 인덱스 ", "disclaimerCompoundIndexesLink": " 복합 인덱스 ",
"disclaimerSuffix": "쿼리 결과를 정렬하는 데만 사용됩니다. 복합 인덱스 추가가 필요한 경우 Mongo 셸을 사용하여 만들 수 있습니다.", "disclaimerSuffix": "쿼리 결과를 정렬하는 데만 사용됩니다. 복합 인덱스 추가해야 하는 경우 MongoDB 셸을 사용하여 생성할 수 있습니다.",
"compoundNotSupported": "복합 인덱스가 있는 컬렉션은 인덱싱 편집기에서 아직 지원되지 않습니다. 이 컬렉션의 인덱싱 정책을 수정하려면 Mongo 을 사용하세요.", "compoundNotSupported": "복합 인덱스가 있는 컬렉션은 아직 인덱싱 편집기에서 지원되지 않습니다. 이 컬렉션의 인덱싱 정책을 수정하려면 Mongo Shell을 사용하세요.",
"aadError": "인덱싱 정책 편집기를 사용하려면 다음에 로그인하세요.", "aadError": "인덱싱 정책 편집기를 사용하려면 다음에 로그인하세요.",
"aadErrorLink": "Azure Portal.", "aadErrorLink": "Azure Portal.",
"refreshingProgress": "인덱스 변환 진행률 새로 고침", "refreshingProgress": "인덱스 변환 진행률 새로 고침",
@@ -992,5 +994,164 @@
"quantizationByteSizeRangeError": "양자화 바이트 크기는 0보다 크고 512 이하이어야 합니다.", "quantizationByteSizeRangeError": "양자화 바이트 크기는 0보다 크고 512 이하이어야 합니다.",
"indexingSearchListSizeRangeError": "인덱싱 검색 목록 크기는 25보다 크거나 같고 500보다 작거나 같아야 합니다." "indexingSearchListSizeRangeError": "인덱싱 검색 목록 크기는 25보다 크거나 같고 500보다 작거나 같아야 합니다."
} }
},
"containerCopy": {
"commandBar": {
"feedbackButtonLabel": "피드백",
"feedbackButtonAriaLabel": "복사 작업에 대한 의견 보내기",
"refreshButtonAriaLabel": "복사 작업 새로 고침",
"createCopyJobButtonLabel": "복사 작업 만들기",
"createCopyJobButtonAriaLabel": "새 컨테이너 복사 작업 만들기"
},
"noCopyJobs": {
"title": "표시할 복사 작업이 없음",
"createCopyJobButtonText": "컨테이너 복사 작업 만들기"
},
"jobDetails": {
"panelTitle": "{{jobName}}",
"panelTitleDefault": "작업 정보",
"errorTitle": "오류 세부 정보",
"selectedContainers": "선택된 컨테이너"
},
"createCopyJob": {
"panelTitle": "복사 작업 만들기"
},
"selectAccount": {
"description": "복사할 대상 계정을 선택하세요.",
"subscriptionDropdownLabel": "구독",
"subscriptionDropdownPlaceholder": "구독 선택",
"accountDropdownLabel": "계정",
"accountDropdownPlaceholder": "계정 선택"
},
"migrationType": {
"offline": {
"title": "오프라인 모드",
"description": "오프라인 컨테이너 복사 작업을 사용하면 지원되는 API에 대해 원본 컨테이너의 데이터를 대상 Cosmos DB 컨테이너로 복사할 수 있습니다. 원본과 대상 간의 데이터 무결성을 보장하려면 복사 작업을 만들기 전에 원본 컨테이너에서 업데이트를 중지하는 것이 좋습니다. [오프라인 복사 작업](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql)에서 자세히 알아보세요."
},
"online": {
"title": "온라인 모드",
"description": "온라인 컨테이너 복사 작업을 사용하면 [모든 버전 및 삭제](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) 변경 피드를 통해 원본 컨테이너의 데이터를 대상 Cosmos DB NoSQL API 컨테이너로 복사할 수 있습니다. 이렇게 하면 데이터가 복사되는 동안에도 원본에서 업데이트를 계속할 수 있습니다. 클라이언트 애플리케이션을 대상 컨테이너로 안전하게 전환하려면 마지막에 잠시 가동 중지 시간이 필요합니다. [온라인 복사 작업](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)에서 자세히 알아보세요."
}
},
"selectContainers": {
"description": "복사할 원본 컨테이너와 대상 컨테이너를 선택하세요.",
"sourceContainerSubHeading": "원본 컨테이너",
"targetContainerSubHeading": "대상 컨테이너",
"databaseDropdownLabel": "데이터베이스",
"databaseDropdownPlaceholder": "데이터베이스 선택",
"containerDropdownLabel": "컨테이너",
"containerDropdownPlaceholder": "컨테이너 선택",
"createNewContainerSubHeading": "대상 계정 '{{accountName}}'의 새 컨테이너의 속성을 구성합니다.",
"createNewContainerSubHeadingDefault": "새 컨테이너의 속성을 구성합니다.",
"createContainerButtonLabel": "새 컨테이너 만들기",
"createContainerHeading": "새 컨테이너 만들기"
},
"preview": {
"jobNameLabel": "작업 이름",
"subscriptionLabel": "대상 구독",
"accountLabel": "대상 계정",
"sourceDatabaseLabel": "원본 데이터베이스",
"sourceContainerLabel": "원본 컨테이너",
"targetDatabaseLabel": "대상 데이터베이스",
"targetContainerLabel": "대상 컨테이너"
},
"assignPermissions": {
"crossAccountDescription": "원본에서 대상 컨테이너로 데이터를 복사하려면 다음 단계를 완료하여 원본 계정의 관리 ID가 대상 계정에 대한 읽기-쓰기 액세스 권한을 갖도록 구성하세요.",
"intraAccountOnlineDescription": "'{{accountName}}' 계정에서 온라인 복사를 사용하도록 설정하려면 아래 단계를 따르세요.",
"crossAccountConfiguration": {
"title": "계정 간 컨테이너 복사",
"description": "아래 지침에 따라 '{{sourceAccount}}'에서 '{{destinationAccount}}'(으)로 데이터를 복사하는 데 필요한 권한을 부여하세요."
},
"onlineConfiguration": {
"title": "온라인 컨테이너 복사",
"description": "'{{accountName}}' 계정에서 온라인 복사를 사용하도록 설정하려면 아래 지침을 따르세요."
}
},
"popoverOverlaySpinnerLabel": "요청을 처리하는 동안 잠시 기다려 주세요...",
"addManagedIdentity": {
"title": "시스템 할당 관리 ID를 사용하도록 설정했습니다.",
"description": "시스템 할당 관리 ID는 리소스당 하나로 제한되며 이 리소스의 수명 주기에 연결됩니다. 사용하도록 설정하면 Azure RBAC(Azure 역할 기반 액세스 제어)를 사용하여 관리 ID에 권한을 부여할 수 있습니다. 관리 ID는 Microsoft Entra ID로 인증되므로 코드에 자격 증명을 저장할 필요가 없습니다.",
"descriptionHrefText": "관리 ID에 대해 자세히 알아보세요.",
"descriptionHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"toggleLabel": "시스템이 할당한 관리 ID",
"tooltipContent": "자세한 정보",
"tooltipHrefText": "관리 ID입니다.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"userAssignedIdentityTooltip": "기존 사용자 할당 ID를 선택하거나 새 ID를 만들 수 있습니다.",
"userAssignedIdentityLabel": "사용자가 할당한 관리 ID를 선택할 수도 있습니다.",
"createUserAssignedIdentityLink": "사용자가 할당한 관리 ID 만들기",
"enablementTitle": "시스템이 할당한 관리 ID 사용",
"enablementDescription": "{{accountName}}에 시스템이 할당한 관리 ID를 사용하도록 설정합니다. 확인하려면 '예' 단추를 클릭하세요."
},
"defaultManagedIdentity": {
"title": "기본값으로 설정된 시스템 할당 관리 ID입니다.",
"description": "시스템이 할당한 관리 ID를 켜서 '{{accountName}}'의 기본값으로 설정합니다.",
"tooltipContent": "자세한 정보",
"tooltipHrefText": "기본 관리 ID입니다.",
"tooltipHref": "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
"popoverTitle": "기본값으로 설정된 시스템 할당 관리 ID",
"popoverDescription": "시스템이 할당한 관리 ID를 '{{accountName}}'의 기본값으로 설정합니다. 확인하려면 '예' 단추를 클릭하세요. "
},
"readWritePermissionAssigned": {
"title": "기본 ID에 할당된 읽기-쓰기 권한입니다.",
"description": "원본에서 대상 컨테이너로 데이터 복사를 허용하려면 원본 계정의 기본 ID에 대상 계정의 읽기-쓰기 액세스 권한을 제공하세요.",
"tooltipContent": "자세한 정보",
"tooltipHrefText": "읽기-쓰기 권한입니다.",
"tooltipHref": "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
"popoverTitle": "기본 ID에 읽기-쓰기 권한을 할당합니다.",
"popoverDescription": "대상 계정의 읽기-쓰기 권한을 원본 계정의 기본 ID에 할당합니다. 확인하려면 '예' 단추를 클릭하세요."
},
"pointInTimeRestore": {
"title": "지정 시간 복원 사용 설정됨",
"description": "온라인 컨테이너 복사 작업을 더 간편하게 수행하려면 '{{accessName}}' 백업 정책을 주기적 백업에서 지속적인 백업으로 업데이트하세요. 이 기능을 사용하려면 지속적인 백업을 사용하도록 설정해야 합니다.",
"tooltipContent": "자세한 정보",
"tooltipHrefText": "지속적인 백업",
"tooltipHref": "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
"buttonText": "지정 시간 복원 사용"
},
"onlineCopyEnabled": {
"title": "온라인 복사 사용 설정됨",
"description": "'{{accountName}}' 계정에서 아래 단추를 클릭하여 온라인 컨테이너 복사를 사용하도록 설정하세요.",
"hrefText": "온라인 복사 작업에 대해 자세히 알아보기",
"href": "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
"buttonText": "온라인 복사 사용",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "모든 버전 및 삭제 변경 피드 모드(미리 보기) 유효성 검사 중...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "모든 버전 및 삭제 변경 피드 모드(미리 보기) 사용 설정 중...",
"enablingOnlineCopySpinnerLabel": "'{{accountName}}' 계정에서 온라인 복사를 사용하도록 설정하는 중..."
},
"monitorJobs": {
"columns": {
"lastUpdatedTime": "날짜 및 시간",
"name": "작업 이름",
"status": "상태",
"completionPercentage": "완료율",
"duration": "기간",
"error": "오류 메시지",
"mode": "모드",
"actions": "작업"
},
"actions": {
"pause": "일시 중지",
"resume": "다시 시작",
"complete": "완료",
"viewDetails": "세부 정보 보기"
},
"status": {
"pending": "큐에 대기됨",
"inProgress": "실행하는 중",
"running": "실행하는 중",
"partitioning": "실행하는 중",
"paused": "일시 중지됨",
"completed": "완료",
"failed": "실패",
"faulted": "실패",
"skipped": "취소됨",
"cancelled": "취소됨"
},
"dialog": {
"confirmButtonText": "확인",
"cancelButtonText": "취소"
}
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More