Compare commits

..

2 Commits

Author SHA1 Message Date
Sung-Hyun Kang
1a1aca5bd5 Added snapshot 2026-02-18 12:26:48 -06:00
Sung-Hyun Kang
e03de89bf3 Remove duplicate span for allowed maximum throughput 2026-02-18 11:07:26 -06:00
195 changed files with 2809 additions and 24025 deletions

1
.gitignore vendored
View File

@@ -17,7 +17,6 @@ Contracts/*
failure.png
screenshots/*
GettingStarted-ignore*.ipynb
src/Localization/Keys.generated.ts
/test-results/
/playwright-report/
/blob-report/

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.25 1.5C9.25 1.22386 9.47386 1 9.75 1H10.25C10.5261 1 10.75 1.22386 10.75 1.5V5.5L13 7.5V9H8.75V14L8 15L7.25 14V9H3V7.5L5.25 5.5V1.5C5.25 1.22386 5.47386 1 5.75 1H6.25C6.52614 1 6.75 1.22386 6.75 1.5V5.25H9.25V1.5Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 354 B

309
package-lock.json generated
View File

@@ -77,7 +77,6 @@
"i18next": "23.11.5",
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "3.0.2",
"i18next-resources-to-backend": "1.2.1",
"iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12",
@@ -181,7 +180,6 @@
"html-inline-css-webpack-plugin": "1.11.2",
"html-loader": "5.0.0",
"html-webpack-plugin": "5.5.3",
"i18next-resources-for-ts": "2.0.0",
"jest": "29.7.0",
"jest-canvas-mock": "2.5.2",
"jest-circus": "29.7.0",
@@ -208,7 +206,6 @@
"typedoc": "0.26.2",
"typescript": "4.9.5",
"url-loader": "4.1.1",
"values-to-keys": "1.1.0",
"wait-on": "9.0.3",
"webpack": "5.104.1",
"webpack-bundle-analyzer": "5.2.0",
@@ -2453,9 +2450,12 @@
"license": "MIT"
},
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
@@ -8684,231 +8684,16 @@
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true
},
"node_modules/@swc/core": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.11.tgz",
"integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.11",
"@swc/core-darwin-x64": "1.15.11",
"@swc/core-linux-arm-gnueabihf": "1.15.11",
"@swc/core-linux-arm64-gnu": "1.15.11",
"@swc/core-linux-arm64-musl": "1.15.11",
"@swc/core-linux-x64-gnu": "1.15.11",
"@swc/core-linux-x64-musl": "1.15.11",
"@swc/core-win32-arm64-msvc": "1.15.11",
"@swc/core-win32-ia32-msvc": "1.15.11",
"@swc/core-win32-x64-msvc": "1.15.11"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz",
"integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.11.tgz",
"integrity": "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz",
"integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz",
"integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz",
"integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz",
"integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz",
"integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz",
"integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz",
"integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz",
"integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true
},
"node_modules/@swc/helpers": {
"version": "0.5.18",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
"version": "0.5.3",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
"tslib": "^2.4.0"
}
},
"node_modules/@swc/helpers/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@swc/types": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"dev": true,
"dependencies": {
"@swc/counter": "^0.1.3"
}
"version": "2.6.2",
"license": "0BSD"
},
"node_modules/@testing-library/dom": {
"version": "7.31.2",
@@ -16876,57 +16661,6 @@
"cross-fetch": "4.0.0"
}
},
"node_modules/i18next-resources-for-ts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-2.0.0.tgz",
"integrity": "sha512-RvATolbJlxrwpZh2+R7ZcNtg0ewmXFFx6rdu9i2bUEBvn6ThgA82rxDe3rJQa3hFS0SopX0qPaABqVDN3TUVpw==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@swc/core": "^1.15.3",
"chokidar": "^5.0.0",
"yaml": "^2.8.2"
},
"bin": {
"i18next-resources-for-ts": "bin/i18next-resources-for-ts.js"
}
},
"node_modules/i18next-resources-for-ts/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/i18next-resources-for-ts/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/i18next-resources-to-backend": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz",
"integrity": "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -25487,7 +25221,6 @@
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"dev": true,
"license": "MIT"
},
"node_modules/regenerator-transform": {
@@ -27965,15 +27698,6 @@
"resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz",
"integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg=="
},
"node_modules/values-to-keys": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/values-to-keys/-/values-to-keys-1.1.0.tgz",
"integrity": "sha512-3ErIYotgwYxBeBstuiaMVDSj6uUzoYnk4A4oM9NqKHrUzSmihvt0DqpQozFq3NWevgnz7hwkOZUXBthIuwxJaQ==",
"dev": true,
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"dev": true,
@@ -29579,17 +29303,14 @@
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
"node": ">= 14"
}
},
"node_modules/yocto-queue": {

View File

@@ -72,7 +72,6 @@
"i18next": "23.11.5",
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "3.0.2",
"i18next-resources-to-backend": "1.2.1",
"iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12",
@@ -189,7 +188,6 @@
"html-inline-css-webpack-plugin": "1.11.2",
"html-loader": "5.0.0",
"html-webpack-plugin": "5.5.3",
"i18next-resources-for-ts": "2.0.0",
"jest": "29.7.0",
"jest-canvas-mock": "2.5.2",
"jest-circus": "29.7.0",
@@ -216,7 +214,6 @@
"typedoc": "0.26.2",
"typescript": "4.9.5",
"url-loader": "4.1.1",
"values-to-keys": "1.1.0",
"wait-on": "9.0.3",
"webpack": "5.104.1",
"webpack-bundle-analyzer": "5.2.0",
@@ -225,8 +222,7 @@
"ws": "8.17.1"
},
"scripts": {
"postinstall": "patch-package && npm run generate:i18n-keys",
"prestart": "npm run generate:i18n-keys",
"postinstall": "patch-package",
"start": "webpack serve --mode development",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci",
@@ -253,7 +249,6 @@
"strict:find": "node ./strict-null-checks/find.js",
"strict:add": "node ./strict-null-checks/auto-add.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generate:i18n-keys": "node utils/generateI18nKeys.mjs",
"generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts"
},
"repository": {

View File

@@ -26,6 +26,15 @@ export default defineConfig({
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
},
{
name: "firefox",
use: {

View File

@@ -1,11 +0,0 @@
import "i18next";
import Resources from "Localization/en/Resources.json";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "Resources";
resources: {
Resources: typeof Resources;
};
}
}

View File

@@ -1,78 +0,0 @@
import { IButtonStyles, IStackStyles, ITextStyles } from "@fluentui/react";
import * as React from "react";
export const getDropdownButtonStyles = (disabled: boolean): IButtonStyles => ({
root: {
width: "100%",
height: "32px",
padding: "0 28px 0 8px",
border: "1px solid #8a8886",
background: "#fff",
color: "#323130",
textAlign: "left",
cursor: disabled ? "not-allowed" : "pointer",
position: "relative",
},
flexContainer: {
justifyContent: "flex-start",
},
label: {
fontWeight: "normal",
fontSize: "14px",
textAlign: "left",
},
});
export const buttonLabelStyles: ITextStyles = {
root: {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "block",
textAlign: "left",
},
};
export const buttonWrapperStyles: React.CSSProperties = {
position: "relative",
width: "100%",
};
export const chevronStyles: React.CSSProperties = {
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "none",
fontSize: "12px",
};
export const calloutContentStyles: IStackStyles = {
root: {
display: "flex",
flexDirection: "column",
},
};
export const listContainerStyles: IStackStyles = {
root: {
maxHeight: "300px",
overflowY: "auto",
},
};
export const getItemStyles = (isSelected: boolean): React.CSSProperties => ({
padding: "8px 12px",
cursor: "pointer",
fontSize: "14px",
backgroundColor: isSelected ? "#e6e6e6" : "transparent",
textAlign: "left",
});
export const emptyMessageStyles: ITextStyles = {
root: {
padding: "8px 12px",
color: "#605e5c",
textAlign: "left",
},
};

View File

@@ -1,200 +0,0 @@
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import { SearchableDropdown } from "./SearchableDropdown";
interface TestItem {
id: string;
name: string;
}
describe("SearchableDropdown", () => {
const mockItems: TestItem[] = [
{ id: "1", name: "Item One" },
{ id: "2", name: "Item Two" },
{ id: "3", name: "Item Three" },
];
const defaultProps = {
label: "Test Label",
items: mockItems,
selectedItem: null as TestItem | null,
onSelect: jest.fn(),
getKey: (item: TestItem) => item.id,
getDisplayText: (item: TestItem) => item.name,
placeholder: "Select an item",
filterPlaceholder: "Filter items",
className: "test-dropdown",
};
beforeEach(() => {
jest.clearAllMocks();
});
it("should render with label and placeholder", () => {
render(<SearchableDropdown {...defaultProps} />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
expect(screen.getByText("Select an item")).toBeInTheDocument();
});
it("should display selected item", () => {
const propsWithSelection = {
...defaultProps,
selectedItem: mockItems[0],
};
render(<SearchableDropdown {...propsWithSelection} />);
expect(screen.getByText("Item One")).toBeInTheDocument();
});
it("should show 'No items found' when items array is empty", () => {
const propsWithEmptyItems = {
...defaultProps,
items: [] as TestItem[],
};
render(<SearchableDropdown {...propsWithEmptyItems} />);
expect(screen.getByText("No Test Labels Found")).toBeInTheDocument();
});
it("should open dropdown when button is clicked", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
expect(screen.getByPlaceholderText("Filter items")).toBeInTheDocument();
});
it("should filter items based on search text", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const searchBox = screen.getByPlaceholderText("Filter items");
fireEvent.change(searchBox, { target: { value: "Two" } });
expect(screen.getByText("Item Two")).toBeInTheDocument();
expect(screen.queryByText("Item One")).not.toBeInTheDocument();
expect(screen.queryByText("Item Three")).not.toBeInTheDocument();
});
it("should call onSelect when an item is clicked", () => {
const onSelectMock = jest.fn();
const propsWithMock = {
...defaultProps,
onSelect: onSelectMock,
};
render(<SearchableDropdown {...propsWithMock} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const item = screen.getByText("Item Two");
fireEvent.click(item);
expect(onSelectMock).toHaveBeenCalledWith(mockItems[1]);
});
it("should close dropdown after selecting an item", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
expect(screen.getByPlaceholderText("Filter items")).toBeInTheDocument();
const item = screen.getByText("Item One");
fireEvent.click(item);
expect(screen.queryByPlaceholderText("Filter items")).not.toBeInTheDocument();
});
it("should disable button when disabled prop is true", () => {
const propsWithDisabled = {
...defaultProps,
disabled: true,
};
render(<SearchableDropdown {...propsWithDisabled} />);
const button = screen.getByRole("button");
expect(button).toBeDisabled();
});
it("should not open dropdown when disabled", () => {
const propsWithDisabled = {
...defaultProps,
disabled: true,
};
render(<SearchableDropdown {...propsWithDisabled} />);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(screen.queryByPlaceholderText("Filter items")).not.toBeInTheDocument();
});
it("should show 'No items found' when search yields no results", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const searchBox = screen.getByPlaceholderText("Filter items");
fireEvent.change(searchBox, { target: { value: "Nonexistent" } });
expect(screen.getByText("No items found")).toBeInTheDocument();
});
it("should handle case-insensitive filtering", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const searchBox = screen.getByPlaceholderText("Filter items");
fireEvent.change(searchBox, { target: { value: "two" } });
expect(screen.getByText("Item Two")).toBeInTheDocument();
expect(screen.queryByText("Item One")).not.toBeInTheDocument();
});
it("should clear filter text when dropdown is closed and reopened", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const searchBox = screen.getByPlaceholderText("Filter items");
fireEvent.change(searchBox, { target: { value: "Two" } });
// Close dropdown by selecting an item
const item = screen.getByText("Item Two");
fireEvent.click(item);
// Reopen dropdown
fireEvent.click(button);
// Filter text should be cleared
const reopenedSearchBox = screen.getByPlaceholderText("Filter items");
expect(reopenedSearchBox).toHaveValue("");
});
it("should use custom placeholder text", () => {
const propsWithCustomPlaceholder = {
...defaultProps,
placeholder: "Choose an option",
};
render(<SearchableDropdown {...propsWithCustomPlaceholder} />);
expect(screen.getByText("Choose an option")).toBeInTheDocument();
});
it("should use custom filter placeholder text", () => {
const propsWithCustomFilterPlaceholder = {
...defaultProps,
filterPlaceholder: "Search here",
};
render(<SearchableDropdown {...propsWithCustomFilterPlaceholder} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
expect(screen.getByPlaceholderText("Search here")).toBeInTheDocument();
});
});

View File

@@ -1,155 +0,0 @@
import {
Callout,
DefaultButton,
DirectionalHint,
Icon,
ISearchBoxStyles,
Label,
SearchBox,
Stack,
Text,
} from "@fluentui/react";
import * as React from "react";
import { useMemo, useRef, useState } from "react";
import {
buttonLabelStyles,
buttonWrapperStyles,
calloutContentStyles,
chevronStyles,
emptyMessageStyles,
getDropdownButtonStyles,
getItemStyles,
listContainerStyles,
} from "./SearchableDropdown.styles";
interface SearchableDropdownProps<T> {
label: string;
items: T[];
selectedItem: T | null;
onSelect: (item: T) => void;
getKey: (item: T) => string;
getDisplayText: (item: T) => string;
placeholder?: string;
filterPlaceholder?: string;
className?: string;
disabled?: boolean;
onDismiss?: () => void;
searchBoxStyles?: Partial<ISearchBoxStyles>;
}
export const SearchableDropdown = <T,>({
label,
items,
selectedItem,
onSelect,
getKey,
getDisplayText,
placeholder = "Select an item",
filterPlaceholder = "Filter items",
className,
disabled = false,
onDismiss,
searchBoxStyles: customSearchBoxStyles,
}: SearchableDropdownProps<T>): React.ReactElement => {
const [isOpen, setIsOpen] = useState(false);
const [filterText, setFilterText] = useState("");
const buttonRef = useRef<HTMLDivElement>(null);
const closeDropdown = () => {
setIsOpen(false);
setFilterText("");
};
const filteredItems = useMemo(
() => items?.filter((item) => getDisplayText(item).toLowerCase().includes(filterText.toLowerCase())),
[items, filterText, getDisplayText],
);
const handleDismiss = () => {
closeDropdown();
onDismiss?.();
};
const handleButtonClick = () => {
if (disabled) {
return;
}
setIsOpen(!isOpen);
};
const handleSelect = (item: T) => {
onSelect(item);
closeDropdown();
};
const buttonLabel = selectedItem
? getDisplayText(selectedItem)
: items?.length === 0
? `No ${label}s Found`
: placeholder;
const buttonId = `${className}-button`;
const buttonStyles = getDropdownButtonStyles(disabled);
return (
<Stack>
<Label htmlFor={buttonId}>{label}</Label>
<div ref={buttonRef} style={buttonWrapperStyles}>
<DefaultButton
id={buttonId}
className={className}
onClick={handleButtonClick}
styles={buttonStyles}
disabled={disabled}
>
<Text styles={buttonLabelStyles}>{buttonLabel}</Text>
</DefaultButton>
<Icon iconName="ChevronDown" style={chevronStyles} />
</div>
{isOpen && (
<Callout
target={buttonRef.current}
onDismiss={handleDismiss}
directionalHint={DirectionalHint.bottomLeftEdge}
isBeakVisible={false}
gapSpace={0}
setInitialFocus
>
<Stack styles={calloutContentStyles} style={{ width: buttonRef.current?.offsetWidth || 300 }}>
<SearchBox
placeholder={filterPlaceholder}
value={filterText}
onChange={(_, newValue) => setFilterText(newValue || "")}
styles={customSearchBoxStyles}
showIcon={true}
/>
<Stack styles={listContainerStyles}>
{filteredItems && filteredItems.length > 0 ? (
filteredItems.map((item) => {
const key = getKey(item);
const isSelected = selectedItem ? getKey(selectedItem) === key : false;
return (
<div
key={key}
onClick={() => handleSelect(item)}
style={getItemStyles(isSelected)}
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "#f3f2f1")}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = isSelected ? "#e6e6e6" : "transparent")
}
>
<Text>{getDisplayText(item)}</Text>
</div>
);
})
) : (
<Text styles={emptyMessageStyles}>No items found</Text>
)}
</Stack>
</Stack>
</Callout>
)}
</Stack>
);
};

View File

@@ -34,7 +34,6 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
databaseId: params.databaseId,
databaseLevelThroughput: params.databaseLevelThroughput,
offerThroughput: params.offerThroughput,
targetAccountOverride: params.targetAccountOverride,
};
await createDatabase(createDatabaseParams);
}
@@ -64,7 +63,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
};
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
if (!params.createNewDatabase && !params.targetAccountOverride) {
if (!params.createNewDatabase) {
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
if (!isValid) {
const collectionName = getCollectionName().toLocaleLowerCase();
@@ -123,9 +122,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
};
const createResponse = await createUpdateSqlContainer(
params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId,
params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup,
params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name,
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload,

View File

@@ -1,134 +0,0 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
jest.mock("../../Utils/arm/generatedClients/cosmos/sqlResources");
import ko from "knockout";
import { AuthType } from "../../AuthType";
import { CreateDatabaseParams, DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { updateUserContext } from "../../UserContext";
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { SqlDatabaseGetResults } from "../../Utils/arm/generatedClients/cosmos/types";
import { createDatabase } from "./createDatabase";
const mockCreateUpdateSqlDatabase = createUpdateSqlDatabase as jest.MockedFunction<typeof createUpdateSqlDatabase>;
describe("createDatabase", () => {
beforeAll(() => {
updateUserContext({
databaseAccount: { name: "default-account" } as DatabaseAccount,
subscriptionId: "default-subscription",
resourceGroup: "default-rg",
apiType: "SQL",
authType: AuthType.AAD,
});
});
beforeEach(() => {
jest.clearAllMocks();
mockCreateUpdateSqlDatabase.mockResolvedValue({
properties: { resource: { id: "db", _rid: "", _self: "", _ts: 0, _etag: "" } },
} as SqlDatabaseGetResults);
useDatabases.setState({
databases: [],
validateDatabaseId: () => true,
} as unknown as ReturnType<typeof useDatabases.getState>);
});
it("should call ARM createUpdateSqlDatabase when logged in with AAD", async () => {
await createDatabase({ databaseId: "testDb" });
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled();
});
describe("targetAccountOverride behavior", () => {
it("should use targetAccountOverride subscriptionId, resourceGroup, and accountName for SQL DB creation", async () => {
const params: CreateDatabaseParams = {
databaseId: "testDb",
targetAccountOverride: {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
},
};
await createDatabase(params);
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
"override-sub",
"override-rg",
"override-account",
"testDb",
expect.any(Object),
);
});
it("should use userContext values when targetAccountOverride is not provided", async () => {
await createDatabase({ databaseId: "testDb" });
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
"default-subscription",
"default-rg",
"default-account",
"testDb",
expect.any(Object),
);
});
it("should skip validateDatabaseId check when targetAccountOverride is provided", async () => {
// Simulate database already existing — validateDatabaseId returns false
useDatabases.setState({
databases: [{ id: ko.observable("testDb") } as unknown as ViewModels.Database],
validateDatabaseId: () => false,
} as unknown as ReturnType<typeof useDatabases.getState>);
const params: CreateDatabaseParams = {
databaseId: "testDb",
targetAccountOverride: {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
},
};
// Should NOT throw even though the normal duplicate check would fail
await expect(createDatabase(params)).resolves.not.toThrow();
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled();
});
it("should throw if validateDatabaseId returns false and no targetAccountOverride is set", async () => {
useDatabases.setState({
databases: [{ id: ko.observable("existingDb") } as unknown as ViewModels.Database],
validateDatabaseId: () => false,
} as unknown as ReturnType<typeof useDatabases.getState>);
await expect(createDatabase({ databaseId: "existingDb" })).rejects.toThrow();
expect(mockCreateUpdateSqlDatabase).not.toHaveBeenCalled();
});
it("should pass databaseId in request payload regardless of targetAccountOverride", async () => {
const params: CreateDatabaseParams = {
databaseId: "my-database",
targetAccountOverride: {
subscriptionId: "any-sub",
resourceGroup: "any-rg",
accountName: "any-account",
},
};
await createDatabase(params);
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
expect.any(String),
expect.any(String),
expect.any(String),
"my-database",
expect.objectContaining({
properties: expect.objectContaining({
resource: expect.objectContaining({ id: "my-database" }),
}),
}),
);
});
});
});

View File

@@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
}
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
if (!params.targetAccountOverride && !useDatabases.getState().validateDatabaseId(params.databaseId)) {
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) {
const databaseName = getDatabaseName().toLocaleLowerCase();
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
}
@@ -72,10 +72,13 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
options,
},
};
const sub = params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
const rg = params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
const acct = params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name;
const createResponse = await createUpdateSqlDatabase(sub, rg, acct, params.databaseId, rpPayload);
const createResponse = await createUpdateSqlDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload,
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}

View File

@@ -1,6 +1,5 @@
import { ContainerResponse } from "@azure/cosmos";
import { Queries } from "Common/Constants";
import * as Logger from "Common/Logger";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
@@ -62,14 +61,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
return await readCollectionsWithARM(databaseId);
}
Logger.logInfo(`readCollections: calling fetchAll for database ${databaseId}`, "readCollections");
const fetchAllStart = Date.now();
const sdkResponse = await client().database(databaseId).containers.readAll().fetchAll();
Logger.logInfo(
`readCollections: fetchAll completed for database ${databaseId}, count=${sdkResponse.resources
?.length}, durationMs=${Date.now() - fetchAllStart}`,
"readCollections",
);
return sdkResponse.resources as DataModels.Collection[];
} catch (error) {
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);

View File

@@ -1,12 +1,11 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { updateUserContext } from "../../UserContext";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
import { readDatabases, readDatabasesForAccount } from "./readDatabases";
import { readDatabases } from "./readDatabases";
describe("readDatabases", () => {
beforeAll(() => {
@@ -43,64 +42,3 @@ describe("readDatabases", () => {
expect(client).toHaveBeenCalled();
});
});
describe("readDatabasesForAccount", () => {
const mockDatabase = { id: "testDb", _rid: "", _self: "", _etag: "", _ts: 0 };
const mockArmResponse = { value: [{ properties: { resource: mockDatabase } }] };
beforeEach(() => {
jest.clearAllMocks();
});
it("should call ARM with a path that includes the provided subscriptionId, resourceGroup, and accountName", async () => {
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
await readDatabasesForAccount("test-sub", "test-rg", "test-account");
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({
path: expect.stringContaining("/subscriptions/test-sub/resourceGroups/test-rg/"),
}),
);
expect(armRequest).toHaveBeenCalledWith(
expect.objectContaining({
path: expect.stringContaining("/databaseAccounts/test-account/sqlDatabases"),
}),
);
});
it("should return mapped database resources from the response", async () => {
const db1 = { id: "db1", _rid: "r1", _self: "/dbs/db1", _etag: "", _ts: 1 };
const db2 = { id: "db2", _rid: "r2", _self: "/dbs/db2", _etag: "", _ts: 2 };
(armRequest as jest.Mock).mockResolvedValue({
value: [{ properties: { resource: db1 } }, { properties: { resource: db2 } }],
});
const result = await readDatabasesForAccount("sub", "rg", "account");
expect(result).toEqual([db1, db2]);
});
it("should return an empty array when the response is null", async () => {
(armRequest as jest.Mock).mockResolvedValue(null);
const result = await readDatabasesForAccount("sub", "rg", "account");
expect(result).toEqual([]);
});
it("should return an empty array when value is an empty list", async () => {
(armRequest as jest.Mock).mockResolvedValue({ value: [] });
const result = await readDatabasesForAccount("sub", "rg", "account");
expect(result).toEqual([]);
});
it("should throw and propagate errors from the ARM call", async () => {
(armRequest as jest.Mock).mockRejectedValue(new Error("ARM request failed"));
await expect(readDatabasesForAccount("sub", "rg", "account")).rejects.toThrow("ARM request failed");
});
});

View File

@@ -112,20 +112,3 @@ async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database);
}
export async function readDatabasesForAccount(
subscriptionId: string,
resourceGroup: string,
accountName: string,
): Promise<DataModels.Database[]> {
const clearMessage = logConsoleProgress(`Querying databases for account ${accountName}`);
try {
const rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? [];
} catch (error) {
handleError(error, "ReadDatabasesForAccount", `Error while querying databases for account ${accountName}`);
throw error;
} finally {
clearMessage();
}
}

View File

@@ -79,10 +79,6 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.fr$`,
`^https:\\/\\/portal\\.sovcloud-azure\\.fr$`,
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.de$`,
`^https:\\/\\/portal\\.sovcloud-azure\\.de$`,
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.sg$`,
`^https:\\/\\/portal\\.sovcloud-azure\\.sg$`,
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",

View File

@@ -404,18 +404,11 @@ export interface AutoPilotOfferSettings {
targetMaxThroughput?: number;
}
export interface AccountOverride {
subscriptionId: string;
resourceGroup: string;
accountName: string;
}
export interface CreateDatabaseParams {
autoPilotMaxThroughput?: number;
databaseId: string;
databaseLevelThroughput?: boolean;
offerThroughput?: number;
targetAccountOverride?: AccountOverride;
}
export interface CreateCollectionParamsBase {
@@ -435,7 +428,6 @@ export interface CreateCollectionParamsBase {
export interface CreateCollectionParams extends CreateCollectionParamsBase {
createNewDatabase: boolean;
collectionId: string;
targetAccountOverride?: AccountOverride;
}
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {

View File

@@ -457,13 +457,13 @@ describe("CopyJobActions", () => {
jobName: "test-job",
migrationType: "online" as any,
source: {
subscriptionId: "sub-123",
subscription: {} as any,
account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: {} as any,
subscriptionId: "sub-123",
account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db",
containerId: "target-container",
@@ -498,7 +498,7 @@ describe("CopyJobActions", () => {
);
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
expect(callArgs.properties.destination.remoteAccountName).toBeUndefined();
expect(callArgs.properties.source.remoteAccountName).toBeUndefined();
expect(mockRefreshJobList).toHaveBeenCalled();
expect(mockOnSuccess).toHaveBeenCalled();
@@ -509,13 +509,13 @@ describe("CopyJobActions", () => {
jobName: "cross-account-job",
migrationType: "offline" as any,
source: {
subscriptionId: "sub-123",
subscription: {} as any,
account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: {} as any,
subscriptionId: "sub-456",
account: { id: "account-2", name: "target-account" } as any,
databaseId: "target-db",
containerId: "target-container",
@@ -528,7 +528,7 @@ describe("CopyJobActions", () => {
await submitCreateCopyJob(mockState, mockOnSuccess);
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
expect(callArgs.properties.destination.remoteAccountName).toBe("target-account");
expect(callArgs.properties.source.remoteAccountName).toBe("source-account");
expect(mockOnSuccess).toHaveBeenCalled();
});
@@ -537,13 +537,13 @@ describe("CopyJobActions", () => {
jobName: "failing-job",
migrationType: "online" as any,
source: {
subscriptionId: "sub-123",
subscription: {} as any,
account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: {} as any,
subscriptionId: "sub-123",
account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db",
containerId: "target-container",
@@ -566,13 +566,13 @@ describe("CopyJobActions", () => {
jobName: "test-job",
migrationType: "online" as any,
source: {
subscriptionId: "sub-123",
subscription: {} as any,
account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: {} as any,
subscriptionId: "sub-123",
account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db",
containerId: "target-container",

View File

@@ -137,12 +137,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
properties: {
source: {
component: "CosmosDBSql",
...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }),
databaseName: source?.databaseId,
containerName: source?.containerId,
},
destination: {
component: "CosmosDBSql",
...(isSameAccount ? {} : { remoteAccountName: target?.account?.name }),
databaseName: target?.databaseId,
containerName: target?.containerId,
},

View File

@@ -185,10 +185,9 @@ describe("CommandBar Utils", () => {
it("should respect disabled state when provided", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
// Theme toggle (index 2) is disabled in Portal mode, others are not
const expectedDisabled = buttons.map((_, index) => index === 2);
const actualDisabled = buttons.map((button) => button.disabled);
expect(actualDisabled).toEqual(expectedDisabled);
buttons.forEach((button) => {
expect(button.disabled).toBe(false);
});
});
it("should return CommandButtonComponentProps with all required properties", () => {

View File

@@ -14,7 +14,6 @@ import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
const isPortal = configContext.platform === Platform.Portal;
const buttons: CopyJobCommandBarBtnType[] = [
{
key: "createCopyJob",
@@ -34,13 +33,8 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
key: "themeToggle",
iconSrc: isDarkMode ? SunIcon : MoonIcon,
label: isDarkMode ? "Light Theme" : "Dark Theme",
ariaLabel: isPortal
? "Dark Mode is managed in Azure Portal Settings"
: isDarkMode
? "Switch to Light Theme"
: "Switch to Dark Theme",
disabled: isPortal,
onClick: isPortal ? () => {} : () => useThemeStore.getState().toggleTheme(),
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme",
onClick: () => useThemeStore.getState().toggleTheme(),
},
];

View File

@@ -20,11 +20,11 @@ export default {
createCopyJobPanelTitle: "Create copy job",
// Select Account Screen
selectAccountDescription: "Please select a destination account to copy to.",
selectAccountDescription: "Please select a source account from which to copy.",
subscriptionDropdownLabel: "Subscription",
subscriptionDropdownPlaceholder: "Select a subscription",
destinationAccountDropdownLabel: "Account",
destinationAccountDropdownPlaceholder: "Select an account",
sourceAccountDropdownLabel: "Account",
sourceAccountDropdownPlaceholder: "Select an account",
migrationTypeOptions: {
offline: {
title: "Offline mode",
@@ -47,17 +47,14 @@ export default {
databaseDropdownPlaceholder: "Select a database",
containerDropdownLabel: "Container",
containerDropdownPlaceholder: "Select a container",
createNewContainerSubHeading: (accountName?: string) =>
accountName
? `Configure the properties for the new container on destination account "${accountName}".`
: "Configure the properties for the new container.",
createNewContainerSubHeading: "Select the properties for your container.",
createContainerButtonLabel: "Create a new container",
createContainerHeading: "Create new container",
// Preview and Create Screen
jobNameLabel: "Job name",
destinationSubscriptionLabel: "Destination subscription",
destinationAccountLabel: "Destination account",
sourceSubscriptionLabel: "Source subscription",
sourceAccountLabel: "Source account",
sourceDatabaseLabel: "Source database",
sourceContainerLabel: "Source container",
targetDatabaseLabel: "Destination database",
@@ -66,7 +63,7 @@ export default {
// 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-write 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 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: {
@@ -119,18 +116,18 @@ export default {
popoverDescription: (accountName: string) =>
`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.",
readPermissionAssigned: {
title: "Read permissions assigned to the default identity.",
description:
"To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.",
"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-write permissions.",
hrefText: "Read permissions.",
href: "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
},
popoverTitle: "Assign read-write permissions to default identity.",
popoverTitle: "Read permissions assigned to default identity.",
popoverDescription:
'Assign read-write permissions on the source account to the default identity of the destination account. To confirm, click the "Yes" button.',
"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",

View File

@@ -59,6 +59,12 @@ describe("CopyJobContext", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "test-subscription-id",
account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
@@ -69,13 +75,7 @@ describe("CopyJobContext", () => {
databaseId: "",
containerId: "",
},
target: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
});
expect(contextValue.flow).toBeNull();
expect(contextValue.contextError).toBeNull();
@@ -598,8 +598,8 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.source?.subscriptionId).toBe("test-subscription-id");
expect(contextValue.copyJobState.source?.account?.name).toBe("test-account");
expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined();
expect(contextValue.copyJobState.source?.account?.name).toBeUndefined();
});
it("should initialize target with userContext values", () => {
@@ -616,11 +616,11 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.target.subscription).toBeNull();
expect(contextValue.copyJobState.target.account).toBeNull();
expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id");
expect(contextValue.copyJobState.target.account.name).toBe("test-account");
});
it("should initialize sourceReadWriteAccessFromTarget as false", () => {
it("should initialize sourceReadAccessFromTarget as false", () => {
let contextValue: any;
render(
@@ -634,7 +634,7 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.sourceReadWriteAccessFromTarget).toBe(false);
expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false);
});
it("should initialize with empty database and container ids", () => {

View File

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

View File

@@ -395,14 +395,6 @@ describe("CopyJobUtils", () => {
expect(result).toBe(false);
});
it("should return false for different completion percentage", () => {
const jobs1 = [createMockJob("job1", "Running")];
const jobs2 = [{ ...createMockJob("job1", "Running"), CompletionPercentage: 75 }];
const result = CopyJobUtils.isEqual(jobs1, jobs2);
expect(result).toBe(false);
});
it("should return true for empty arrays", () => {
const result = CopyJobUtils.isEqual([], []);
expect(result).toBe(true);

View File

@@ -142,7 +142,7 @@ export function isEqual(prevJobs: CopyJobType[], newJobs: CopyJobType[]): boolea
if (!newJob) {
return false;
}
return prevJob.Status === newJob.Status && prevJob.CompletionPercentage === newJob.CompletionPercentage;
return prevJob.Status === newJob.Status;
});
}

View File

@@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => {
databaseId: "target-db",
containerId: "target-container",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
};
const mockContextValue = {

View File

@@ -4,7 +4,7 @@ import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes";
import AddReadWritePermissionToDefaultIdentity from "./AddReadWritePermissionToDefaultIdentity";
import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity";
jest.mock("../../../../../Common/Logger", () => ({
logError: jest.fn(),
@@ -73,7 +73,7 @@ import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUti
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import useToggle from "./hooks/useToggle";
describe("AddReadWritePermissionToDefaultIdentity Component", () => {
describe("AddReadPermissionToDefaultIdentity Component", () => {
const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>;
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
@@ -86,7 +86,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "source-sub-id",
subscription: { subscriptionId: "source-sub-id" } as Subscription,
account: {
id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
name: "source-account",
@@ -101,7 +101,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
containerId: "source-container",
},
target: {
subscription: { subscriptionId: "target-sub-id" } as Subscription,
subscriptionId: "target-sub-id",
account: {
id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
name: "target-account",
@@ -119,7 +119,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
databaseId: "target-db",
containerId: "target-container",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
},
setCopyJobState: jest.fn(),
setContextError: jest.fn(),
@@ -133,7 +133,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
const renderComponent = (contextValue = mockContextValue) => {
return render(
<CopyJobContext.Provider value={contextValue}>
<AddReadWritePermissionToDefaultIdentity />
<AddReadPermissionToDefaultIdentity />
</CopyJobContext.Provider>,
);
};
@@ -164,12 +164,12 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
expect(container).toMatchSnapshot();
});
it("should render correctly when sourceReadWriteAccessFromTarget is true", () => {
it("should render correctly when sourceReadAccessFromTarget is true", () => {
const contextWithAccess = {
...mockContextValue,
copyJobState: {
...mockContextValue.copyJobState,
sourceReadWriteAccessFromTarget: true,
sourceReadAccessFromTarget: true,
},
};
const { container } = renderComponent(contextWithAccess);
@@ -180,7 +180,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
describe("Component Structure", () => {
it("should display the description text", () => {
renderComponent();
expect(screen.getByText(ContainerCopyMessages.readWritePermissionAssigned.description)).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument();
});
it("should display the info tooltip", () => {
@@ -212,10 +212,10 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
expect(screen.getByTestId("popover-message")).toBeInTheDocument();
expect(screen.getByTestId("popover-title")).toHaveTextContent(
ContainerCopyMessages.readWritePermissionAssigned.popoverTitle,
ContainerCopyMessages.readPermissionAssigned.popoverTitle,
);
expect(screen.getByTestId("popover-content")).toHaveTextContent(
ContainerCopyMessages.readWritePermissionAssigned.popoverDescription,
ContainerCopyMessages.readPermissionAssigned.popoverDescription,
);
});
@@ -243,7 +243,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
expect(mockOnToggle).toHaveBeenCalledWith(null, false);
});
it("should call handleAddReadWritePermission when primary button is clicked", async () => {
it("should call handleAddReadPermission when primary button is clicked", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
@@ -264,7 +264,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
});
});
describe("handleAddReadWritePermission Function", () => {
describe("handleAddReadPermission Function", () => {
beforeEach(() => {
mockUseToggle.mockReturnValue([true, jest.fn()]);
});
@@ -312,7 +312,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith(
"Permission denied",
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
);
});
@@ -336,14 +336,14 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith(
"Error assigning read-write permission to default identity. Please try again later.",
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
"Error assigning read permission to default identity. Please try again later.",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
);
});
await waitFor(() => {
expect(mockContextValue.setContextError).toHaveBeenCalledWith(
"Error assigning read-write permission to default identity. Please try again later.",
"Error assigning read permission to default identity. Please try again later.",
);
});
});
@@ -496,7 +496,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
expect(updatedState).toEqual({
...mockContextValue.copyJobState,
sourceReadWriteAccessFromTarget: true,
sourceReadAccessFromTarget: true,
});
});
});

View File

@@ -12,29 +12,27 @@ import useToggle from "./hooks/useToggle";
const TooltipContent = (
<Text>
{ContainerCopyMessages.readWritePermissionAssigned.tooltip.content} &nbsp;
{ContainerCopyMessages.readPermissionAssigned.tooltip.content} &nbsp;
<Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.readWritePermissionAssigned.tooltip.href}
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.readWritePermissionAssigned.tooltip.hrefText}
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
</Link>
</Text>
);
type AddReadPermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
type AddReadWritePermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionToDefaultIdentityProps> = () => {
const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIdentityProps> = () => {
const [loading, setLoading] = React.useState(false);
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
const [readWritePermissionAssigned, onToggle] = useToggle(copyJobState.sourceReadWriteAccessFromTarget ?? false);
const [readPermissionAssigned, onToggle] = useToggle(false);
const handleAddReadWritePermission = async () => {
const handleAddReadPermission = async () => {
const { source, target } = copyJobState;
const selectedSourceAccount = source?.account;
try {
const {
subscriptionId: sourceSubscriptionId,
@@ -49,17 +47,16 @@ const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionTo
sourceAccountName,
target?.account?.identity?.principalId ?? "",
);
if (assignedRole) {
setCopyJobState((prevState) => ({
...prevState,
sourceReadWriteAccessFromTarget: true,
sourceReadAccessFromTarget: true,
}));
}
} catch (error) {
const errorMessage =
error.message || "Error assigning read-write permission to default identity. Please try again later.";
logError(errorMessage, "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission");
error.message || "Error assigning read permission to default identity. Please try again later.";
logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission");
setContextError(errorMessage);
} finally {
setLoading(false);
@@ -69,12 +66,12 @@ const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionTo
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Text className="toggle-label">
{ContainerCopyMessages.readWritePermissionAssigned.description}&ensp;
{ContainerCopyMessages.readPermissionAssigned.description}&ensp;
<InfoTooltip content={TooltipContent} />
</Text>
<Toggle
data-test="btn-toggle"
checked={readWritePermissionAssigned}
checked={readPermissionAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}
onChange={onToggle}
@@ -86,15 +83,15 @@ const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionTo
/>
<PopoverMessage
isLoading={loading}
visible={readWritePermissionAssigned}
title={ContainerCopyMessages.readWritePermissionAssigned.popoverTitle}
visible={readPermissionAssigned}
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
onCancel={() => onToggle(null, false)}
onPrimary={handleAddReadWritePermission}
onPrimary={handleAddReadPermission}
>
{ContainerCopyMessages.readWritePermissionAssigned.popoverDescription}
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
</PopoverMessage>
</Stack>
);
};
export default AddReadWritePermissionToDefaultIdentity;
export default AddReadPermissionToDefaultIdentity;

View File

@@ -43,12 +43,12 @@ jest.mock("./AddManagedIdentity", () => {
return MockAddManagedIdentity;
});
jest.mock("./AddReadWritePermissionToDefaultIdentity", () => {
const MockAddReadWritePermissionToDefaultIdentity = () => {
return <div data-testid="add-read-write-permission">Add Read-Write Permission Component</div>;
jest.mock("./AddReadPermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">Add Read Permission Component</div>;
};
MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
return MockAddReadWritePermissionToDefaultIdentity;
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity;
});
jest.mock("./DefaultManagedIdentity", () => {
@@ -85,18 +85,18 @@ describe("AssignPermissions Component", () => {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "source-sub",
subscription: { subscriptionId: "source-sub" } as any,
account: { id: "source-account", name: "Source Account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: { subscriptionId: "target-sub" } as any,
subscriptionId: "target-sub",
account: { id: "target-account", name: "Target Account" } as any,
databaseId: "target-db",
containerId: "target-container",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
...overrides,
});
@@ -164,13 +164,13 @@ describe("AssignPermissions Component", () => {
const copyJobState = createMockCopyJobState({
migrationType: CopyJobMigrationType.Online,
source: {
subscriptionId: "same-sub",
subscription: { subscriptionId: "same-sub" } as any,
account: { id: "same-account", name: "Same Account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: { subscriptionId: "same-sub" } as any,
subscriptionId: "same-sub",
account: { id: "same-account", name: "Same Account" } as any,
databaseId: "target-db",
containerId: "target-container",
@@ -201,7 +201,7 @@ describe("AssignPermissions Component", () => {
completed: true,
},
{
id: "readWritePermissionAssigned",
id: "readPermissionAssigned",
title: "Read Permission Assigned",
Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>,
disabled: false,
@@ -347,7 +347,7 @@ describe("AssignPermissions Component", () => {
it("should handle missing account names", () => {
const copyJobState = createMockCopyJobState({
source: {
subscriptionId: "source-sub",
subscription: { subscriptionId: "source-sub" } as any,
account: { id: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",

View File

@@ -50,18 +50,18 @@ describe("PointInTimeRestore", () => {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "test-sub",
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
account: mockSourceAccount,
databaseId: "test-db",
containerId: "test-container",
},
target: {
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
subscriptionId: "test-sub",
account: mockSourceAccount,
databaseId: "target-db",
containerId: "target-container",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
} as CopyJobContextState;
const mockSetCopyJobState = jest.fn();

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -8,7 +8,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
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.
<div
data-testid="info-tooltip"
@@ -24,7 +24,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
rel="noopener noreferrer"
target="_blank"
>
Read-write permissions.
Read permissions.
</a>
</span>
</div>
@@ -63,7 +63,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
</div>
`;
exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -71,7 +71,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
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.
<div
data-testid="info-tooltip"
@@ -87,7 +87,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
rel="noopener noreferrer"
target="_blank"
>
Read-write permissions.
Read permissions.
</a>
</span>
</div>
@@ -126,7 +126,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
</div>
`;
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when sourceReadWriteAccessFromTarget is true 1`] = `
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when sourceReadAccessFromTarget is true 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -134,7 +134,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
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.
<div
data-testid="info-tooltip"
@@ -150,7 +150,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
rel="noopener noreferrer"
target="_blank"
>
Read-write permissions.
Read permissions.
</a>
</span>
</div>
@@ -189,7 +189,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
</div>
`;
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -197,7 +197,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
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.
<div
data-testid="info-tooltip"
@@ -213,7 +213,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
rel="noopener noreferrer"
target="_blank"
>
Read-write permissions.
Read permissions.
</a>
</span>
</div>
@@ -255,12 +255,12 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
<div
data-testid="popover-title"
>
Assign read-write permissions to default identity.
Read permissions assigned to default identity.
</div>
<div
data-testid="popover-content"
>
Assign read-write permissions on the source account to the default identity of the destination account. To confirm, click the "Yes" button.
Assign read permissions of the source account to the default identity of the destination account. To confirm click the Yes button.
</div>
<button
data-testid="popover-cancel"
@@ -277,7 +277,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
</div>
`;
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -285,7 +285,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
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.
<div
data-testid="info-tooltip"
@@ -301,7 +301,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
rel="noopener noreferrer"
target="_blank"
>
Read-write permissions.
Read permissions.
</a>
</span>
</div>
@@ -340,7 +340,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
</div>
`;
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -348,7 +348,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
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.
<div
data-testid="info-tooltip"
@@ -364,7 +364,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
rel="noopener noreferrer"
target="_blank"
>
Read-write permissions.
Read permissions.
</a>
</span>
</div>

View File

@@ -9,7 +9,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write 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 destination account has read access to the source account by completing the following steps.
</span>
<div
class="ms-Stack css-111"
@@ -212,7 +212,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write 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 destination account has read access to the source account by completing the following steps.
</span>
<div
class="ms-Stack css-111"
@@ -618,7 +618,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write 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 destination account has read access to the source account by completing the following steps.
</span>
<div
class="ms-Stack css-111"
@@ -1153,7 +1153,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write 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 destination account has read access to the source account by completing the following steps.
</span>
<div
class="ms-Stack css-111"
@@ -1307,7 +1307,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write 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 destination account has read access to the source account by completing the following steps.
</span>
<div
data-testid="shimmer-tree"
@@ -1329,7 +1329,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write 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 destination account has read access to the source account by completing the following steps.
</span>
<div
data-testid="shimmer-tree"

View File

@@ -13,7 +13,7 @@ import {
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache";
import usePermissionSections, {
checkTargetHasReadWriteRoleOnSource,
checkTargetHasReaderRoleOnSource,
PermissionGroupConfig,
SECTION_IDS,
} from "./usePermissionsSection";
@@ -40,12 +40,12 @@ jest.mock("../AddManagedIdentity", () => {
return MockAddManagedIdentity;
});
jest.mock("../AddReadWritePermissionToDefaultIdentity", () => {
const MockAddReadWritePermissionToDefaultIdentity = () => {
return <div data-testid="add-read-write-permission">AddReadWritePermissionToDefaultIdentity</div>;
jest.mock("../AddReadPermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">AddReadPermissionToDefaultIdentity</div>;
};
MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
return MockAddReadWritePermissionToDefaultIdentity;
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity;
});
jest.mock("../DefaultManagedIdentity", () => {
@@ -133,7 +133,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscriptionId: "",
subscription: undefined,
databaseId: "",
containerId: "",
},
@@ -152,7 +152,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscription: undefined,
subscriptionId: "",
databaseId: "",
containerId: "",
},
@@ -193,7 +193,7 @@ describe("usePermissionsSection", () => {
expect(capturedResult[0].sections.map((s) => s.id)).toEqual([
SECTION_IDS.addManagedIdentity,
SECTION_IDS.defaultManagedIdentity,
SECTION_IDS.readWritePermissionAssigned,
SECTION_IDS.readPermissionAssigned,
]);
});
@@ -208,7 +208,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscriptionId: "",
subscription: undefined,
databaseId: "",
containerId: "",
},
@@ -222,7 +222,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscription: undefined,
subscriptionId: "",
databaseId: "",
containerId: "",
},
@@ -299,7 +299,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscription: undefined,
subscriptionId: "",
databaseId: "",
containerId: "",
},
@@ -337,7 +337,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscription: undefined,
subscriptionId: "",
databaseId: "",
containerId: "",
},
@@ -358,17 +358,16 @@ describe("usePermissionsSection", () => {
expect(defaultManagedIdentitySection?.completed).toBe(true);
});
it("should validate readWritePermissionAssigned section with contributor role", async () => {
it("should validate readPermissionAssigned section with reader role", async () => {
const mockRoleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "00000000-0000-0000-0000-000000000002",
name: "Custom Role",
permissions: [
{
dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
],
},
],
@@ -399,7 +398,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscription: undefined,
subscriptionId: "",
databaseId: "",
containerId: "",
},
@@ -408,9 +407,7 @@ describe("usePermissionsSection", () => {
render(<TestWrapper state={state} onResult={noop} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.readWritePermissionAssigned}-completed`)).toHaveTextContent(
"true",
);
expect(screen.getByTestId(`section-${SECTION_IDS.readPermissionAssigned}-completed`)).toHaveTextContent("true");
});
expect(mockedRbacUtils.fetchRoleAssignments).toHaveBeenCalledWith(
@@ -438,7 +435,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscriptionId: "",
subscription: undefined,
databaseId: "",
containerId: "",
},
@@ -479,7 +476,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscriptionId: "",
subscription: undefined,
databaseId: "",
containerId: "",
},
@@ -549,7 +546,7 @@ describe("usePermissionsSection", () => {
type: "",
kind: "",
},
subscription: undefined,
subscriptionId: "",
databaseId: "",
containerId: "",
},
@@ -571,12 +568,12 @@ describe("usePermissionsSection", () => {
});
});
describe("checkTargetHasReadWriteRoleOnSource", () => {
it("should return true for built-in Contributor role", () => {
describe("checkTargetHasReaderRoleOnSource", () => {
it("should return true for built-in Reader role", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "00000000-0000-0000-0000-000000000002",
name: "00000000-0000-0000-0000-000000000001",
permissions: [],
assignableScopes: [],
resourceGroup: "",
@@ -586,21 +583,20 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
},
];
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
it("should return true for custom role with read-write data actions", () => {
it("should return true for custom role with required data actions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "Custom Contributor Role",
name: "Custom Reader Role",
permissions: [
{
dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
],
},
],
@@ -612,7 +608,7 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
},
];
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
@@ -634,12 +630,12 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
},
];
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(false);
});
it("should return false for empty role definitions", () => {
const result = checkTargetHasReadWriteRoleOnSource([]);
const result = checkTargetHasReaderRoleOnSource([]);
expect(result).toBe(false);
});
@@ -657,11 +653,11 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
},
];
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(false);
});
it("should handle multiple roles and return true if any has sufficient read-write permissions", () => {
it("should handle multiple roles and return true if any has sufficient permissions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
@@ -679,7 +675,7 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
},
{
id: "role-2",
name: "00000000-0000-0000-0000-000000000002",
name: "00000000-0000-0000-0000-000000000001",
permissions: [],
assignableScopes: [],
resourceGroup: "",
@@ -689,7 +685,7 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
},
];
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
});

View File

@@ -12,7 +12,7 @@ import {
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
import AddManagedIdentity from "../AddManagedIdentity";
import AddReadWritePermissionToDefaultIdentity from "../AddReadWritePermissionToDefaultIdentity";
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
import DefaultManagedIdentity from "../DefaultManagedIdentity";
import OnlineCopyEnabled from "../OnlineCopyEnabled";
import PointInTimeRestore from "../PointInTimeRestore";
@@ -36,13 +36,11 @@ export interface PermissionGroupConfig {
export const SECTION_IDS = {
addManagedIdentity: "addManagedIdentity",
defaultManagedIdentity: "defaultManagedIdentity",
readWritePermissionAssigned: "readWritePermissionAssigned",
readPermissionAssigned: "readPermissionAssigned",
pointInTimeRestore: "pointInTimeRestore",
onlineCopyEnabled: "onlineCopyEnabled",
} as const;
const COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID = "00000000-0000-0000-0000-000000000002";
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
{
id: SECTION_IDS.addManagedIdentity,
@@ -68,9 +66,9 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
},
},
{
id: SECTION_IDS.readWritePermissionAssigned,
title: ContainerCopyMessages.readWritePermissionAssigned.title,
Component: AddReadWritePermissionToDefaultIdentity,
id: SECTION_IDS.readPermissionAssigned,
title: ContainerCopyMessages.readPermissionAssigned.title,
Component: AddReadPermissionToDefaultIdentity,
disabled: true,
validate: async (state: CopyJobContextState) => {
const principalId = state?.target?.account?.identity?.principalId;
@@ -89,7 +87,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
);
const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []);
return checkTargetHasReadWriteRoleOnSource(roleDefinitions ?? []);
return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []);
},
},
];
@@ -121,34 +119,18 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
];
/**
* Checks if the user has contributor-style read-write access on the source account.
* Checks if the user has the Reader role based on role definitions.
*/
export function checkTargetHasReadWriteRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some((role) => {
if (role.name === COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID) {
return true;
}
const dataActions = role.permissions?.flatMap((permission) => permission.dataActions ?? []) ?? [];
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;
});
export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some(
(role) =>
role.name === "00000000-0000-0000-0000-000000000001" ||
role.permissions.some(
(permission) =>
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata") &&
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read"),
),
);
}
/**

View File

@@ -81,7 +81,7 @@ describe("AddCollectionPanelWrapper", () => {
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
},
setCopyJobState: mockSetCopyJobState,
flow: null,
@@ -109,7 +109,7 @@ describe("AddCollectionPanelWrapper", () => {
expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument();
expect(container.querySelector(".addCollectionPanelHeader")).toBeInTheDocument();
expect(container.querySelector(".addCollectionPanelBody")).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.createNewContainerSubHeading())).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.createNewContainerSubHeading)).toBeInTheDocument();
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
});

View File

@@ -1,14 +1,11 @@
import { IDropdownOption, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { readDatabasesForAccount } from "Common/dataAccess/readDatabases";
import { AccountOverride } from "Contracts/DataModels";
import { Stack, Text } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { useSidePanel } from "hooks/useSidePanel";
import { produce } from "immer";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect } from "react";
import { AddCollectionPanel } from "../../../../Panes/AddCollectionPanel/AddCollectionPanel";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
type AddCollectionPanelWrapperProps = {
explorer?: Explorer;
@@ -16,26 +13,7 @@ type AddCollectionPanelWrapperProps = {
};
const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapperProps> = ({ explorer, goBack }) => {
const { setCopyJobState, copyJobState } = useCopyJobContext();
const [destinationDatabases, setDestinationDatabases] = useState<IDropdownOption[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [permissionError, setPermissionError] = useState<string | null>(null);
const targetAccountOverride: AccountOverride | undefined = useMemo(() => {
const accountId = copyJobState?.target?.account?.id;
if (!accountId) {
return undefined;
}
const details = getAccountDetailsFromResourceId(accountId);
if (!details?.subscriptionId || !details?.resourceGroup || !details?.accountName) {
return undefined;
}
return {
subscriptionId: details.subscriptionId,
resourceGroup: details.resourceGroup,
accountName: details.accountName,
};
}, [copyJobState?.target?.account?.id]);
const { setCopyJobState } = useCopyJobContext();
useEffect(() => {
const sidePanelStore = useSidePanel.getState();
@@ -47,52 +25,6 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
};
}, []);
useEffect(() => {
if (!targetAccountOverride) {
setIsLoading(false);
return undefined;
}
let cancelled = false;
const fetchDatabases = async () => {
setIsLoading(true);
setPermissionError(null);
try {
const databases = await readDatabasesForAccount(
targetAccountOverride.subscriptionId,
targetAccountOverride.resourceGroup,
targetAccountOverride.accountName,
);
if (!cancelled) {
setDestinationDatabases(databases.map((db) => ({ key: db.id, text: db.id })));
}
} catch (error) {
if (!cancelled) {
const message = error?.message || String(error);
if (message.includes("AuthorizationFailed") || message.includes("403")) {
setPermissionError(
`You do not have sufficient permissions to access the destination account "${targetAccountOverride.accountName}". ` +
"Please ensure you have at least Contributor or Owner access to create databases and containers.",
);
} else {
setPermissionError(
`Failed to load databases from the destination account "${targetAccountOverride.accountName}": ${message}`,
);
}
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
};
fetchDatabases();
return () => {
cancelled = true;
};
}, [targetAccountOverride]);
const handleAddCollectionSuccess = useCallback(
(collectionData: { databaseId: string; collectionId: string }) => {
setCopyJobState(
@@ -106,37 +38,13 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
[goBack],
);
if (isLoading) {
return (
<Stack horizontalAlign="center" verticalAlign="center" styles={{ root: { padding: 20 } }}>
<Spinner size={SpinnerSize.large} label="Loading destination account databases..." />
</Stack>
);
}
if (permissionError) {
return (
<Stack styles={{ root: { padding: 20 } }}>
<MessageBar messageBarType={MessageBarType.error}>{permissionError}</MessageBar>
</Stack>
);
}
return (
<Stack className="addCollectionPanelWrapper">
<Stack.Item className="addCollectionPanelHeader">
<Text className="themeText">
{ContainerCopyMessages.createNewContainerSubHeading(targetAccountOverride?.accountName)}
</Text>
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
</Stack.Item>
<Stack.Item className="addCollectionPanelBody">
<AddCollectionPanel
explorer={explorer}
isCopyJobFlow={true}
onSubmitSuccess={handleAddCollectionSuccess}
targetAccountOverride={targetAccountOverride}
externalDatabaseOptions={destinationDatabases}
/>
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
</Stack.Item>
</Stack>
);

View File

@@ -3,19 +3,19 @@
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`] = `
<div>
<div
class="ms-Stack addCollectionPanelWrapper css-115"
class="ms-Stack addCollectionPanelWrapper css-109"
>
<div
class="ms-StackItem addCollectionPanelHeader css-116"
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="themeText css-117"
class="themeText css-111"
>
Configure the properties for the new container.
Select the properties for your container.
</span>
</div>
<div
class="ms-StackItem addCollectionPanelBody css-116"
class="ms-StackItem addCollectionPanelBody css-110"
>
<div
data-testid="add-collection-panel"
@@ -44,19 +44,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with both props 1`] = `
<div>
<div
class="ms-Stack addCollectionPanelWrapper css-115"
class="ms-Stack addCollectionPanelWrapper css-109"
>
<div
class="ms-StackItem addCollectionPanelHeader css-116"
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="themeText css-117"
class="themeText css-111"
>
Configure the properties for the new container.
Select the properties for your container.
</span>
</div>
<div
class="ms-StackItem addCollectionPanelBody css-116"
class="ms-StackItem addCollectionPanelBody css-110"
>
<div
data-testid="add-collection-panel"
@@ -85,19 +85,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with explorer prop 1`] = `
<div>
<div
class="ms-Stack addCollectionPanelWrapper css-115"
class="ms-Stack addCollectionPanelWrapper css-109"
>
<div
class="ms-StackItem addCollectionPanelHeader css-116"
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="themeText css-117"
class="themeText css-111"
>
Configure the properties for the new container.
Select the properties for your container.
</span>
</div>
<div
class="ms-StackItem addCollectionPanelBody css-116"
class="ms-StackItem addCollectionPanelBody css-110"
>
<div
data-testid="add-collection-panel"
@@ -126,19 +126,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with goBack prop 1`] = `
<div>
<div
class="ms-Stack addCollectionPanelWrapper css-115"
class="ms-Stack addCollectionPanelWrapper css-109"
>
<div
class="ms-StackItem addCollectionPanelHeader css-116"
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="themeText css-117"
class="themeText css-111"
>
Configure the properties for the new container.
Select the properties for your container.
</span>
</div>
<div
class="ms-StackItem addCollectionPanelBody css-116"
class="ms-StackItem addCollectionPanelBody css-110"
>
<div
data-testid="add-collection-panel"

View File

@@ -87,18 +87,18 @@ describe("PreviewCopyJob", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "test-subscription-id",
subscription: mockSubscription,
account: mockDatabaseAccount,
databaseId: "source-database",
containerId: "source-container",
},
target: {
subscription: mockSubscription,
subscriptionId: "test-subscription-id",
account: mockDatabaseAccount,
databaseId: "target-database",
containerId: "target-container",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
...overrides,
};
@@ -146,7 +146,7 @@ describe("PreviewCopyJob", () => {
it("should render with missing source subscription information", () => {
const mockContext = createMockContext({
source: {
subscriptionId: "",
subscription: undefined,
account: mockDatabaseAccount,
databaseId: "source-database",
containerId: "source-container",
@@ -165,7 +165,7 @@ describe("PreviewCopyJob", () => {
it("should render with missing source account information", () => {
const mockContext = createMockContext({
source: {
subscriptionId: "test-subscription-id",
subscription: mockSubscription,
account: null,
databaseId: "source-database",
containerId: "source-container",
@@ -184,13 +184,13 @@ describe("PreviewCopyJob", () => {
it("should render with undefined database and container names", () => {
const mockContext = createMockContext({
source: {
subscriptionId: "test-subscription-id",
subscription: mockSubscription,
account: mockDatabaseAccount,
databaseId: "",
containerId: "",
},
target: {
subscription: mockSubscription,
subscriptionId: "test-subscription-id",
account: mockDatabaseAccount,
databaseId: "",
containerId: "",
@@ -219,7 +219,7 @@ describe("PreviewCopyJob", () => {
const mockContext = createMockContext({
source: {
subscriptionId: longNameSubscription.subscriptionId,
subscription: longNameSubscription,
account: longNameAccount,
databaseId: "long-database-name-for-testing-purposes",
containerId: "long-container-name-for-testing-purposes",
@@ -253,13 +253,13 @@ describe("PreviewCopyJob", () => {
it("should handle special characters in database and container names", () => {
const mockContext = createMockContext({
source: {
subscriptionId: "test-subscription-id",
subscription: mockSubscription,
account: mockDatabaseAccount,
databaseId: "test-db_with@special#chars",
containerId: "test-container_with@special#chars",
},
target: {
subscription: mockSubscription,
subscriptionId: "test-subscription-id",
account: mockDatabaseAccount,
databaseId: "target-db_with@special#chars",
containerId: "target-container_with@special#chars",
@@ -285,12 +285,12 @@ describe("PreviewCopyJob", () => {
const mockContext = createMockContext({
target: {
subscription: mockSubscription,
subscriptionId: "target-subscription-id",
account: targetAccount,
databaseId: "target-database",
containerId: "target-container",
},
sourceReadWriteAccessFromTarget: true,
sourceReadAccessFromTarget: true,
});
const { container } = render(
@@ -360,7 +360,7 @@ describe("PreviewCopyJob", () => {
);
expect(getByText(/Job name/i)).toBeInTheDocument();
expect(getByText(/Destination subscription/i)).toBeInTheDocument();
expect(getByText(/Destination account/i)).toBeInTheDocument();
expect(getByText(/Source subscription/i)).toBeInTheDocument();
expect(getByText(/Source account/i)).toBeInTheDocument();
});
});

View File

@@ -36,15 +36,15 @@ const PreviewCopyJob: React.FC = () => {
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
</FieldRow>
<Stack>
<Text className="bold themeText">{ContainerCopyMessages.destinationSubscriptionLabel}</Text>
<Text data-test="destination-subscription-name" className="themeText">
{copyJobState.target?.subscription?.displayName}
<Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
<Text data-test="source-subscription-name" className="themeText">
{copyJobState.source?.subscription?.displayName}
</Text>
</Stack>
<Stack>
<Text className="bold themeText">{ContainerCopyMessages.destinationAccountLabel}</Text>
<Text data-test="destination-account-name" className="themeText">
{copyJobState.target?.account?.name}
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text data-test="source-account-name" className="themeText">
{copyJobState.source?.account?.name}
</Text>
</Stack>
<Stack>

View File

@@ -49,11 +49,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
<span
class="bold themeText css-125"
>
Destination subscription
Source subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -64,11 +64,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
<span
class="bold themeText css-125"
>
Destination account
Source account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
data-test="source-account-name"
>
test-account
</span>
@@ -371,11 +371,11 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
<span
class="bold themeText css-125"
>
Destination subscription
Source subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -386,13 +386,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
<span
class="bold themeText css-125"
>
Destination account
Source account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
data-test="source-account-name"
>
target-account
test-account
</span>
</div>
<div
@@ -693,11 +693,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
<span
class="bold themeText css-125"
>
Destination subscription
Source subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -708,11 +708,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
<span
class="bold themeText css-125"
>
Destination account
Source account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
data-test="source-account-name"
>
test-account
</span>
@@ -1015,13 +1015,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
<span
class="bold themeText css-125"
>
Destination subscription
Source subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
data-test="source-subscription-name"
>
Test Subscription
This is a very long subscription name that might cause display issues if not handled properly
</span>
</div>
<div
@@ -1030,13 +1030,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
<span
class="bold themeText css-125"
>
Destination account
Source account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
data-test="source-account-name"
>
test-account
this-is-a-very-long-database-account-name-that-might-cause-display-issues
</span>
</div>
<div
@@ -1337,11 +1337,11 @@ exports[`PreviewCopyJob should render with missing source account information 1`
<span
class="bold themeText css-125"
>
Destination subscription
Source subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -1352,13 +1352,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
<span
class="bold themeText css-125"
>
Destination account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
>
test-account
Source account
</span>
</div>
<div
@@ -1659,13 +1653,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
<span
class="bold themeText css-125"
>
Destination subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
>
Test Subscription
Source subscription
</span>
</div>
<div
@@ -1674,11 +1662,11 @@ exports[`PreviewCopyJob should render with missing source subscription informati
<span
class="bold themeText css-125"
>
Destination account
Source account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
data-test="source-account-name"
>
test-account
</span>
@@ -1981,11 +1969,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
<span
class="bold themeText css-125"
>
Destination subscription
Source subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -1996,11 +1984,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
<span
class="bold themeText css-125"
>
Destination account
Source account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
data-test="source-account-name"
>
test-account
</span>
@@ -2303,11 +2291,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<span
class="bold themeText css-125"
>
Destination subscription
Source subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -2318,11 +2306,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<span
class="bold themeText css-125"
>
Destination account
Source account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
data-test="source-account-name"
>
test-account
</span>
@@ -2625,11 +2613,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
<span
class="bold themeText css-125"
>
Destination subscription
Source subscription
</span>
<span
class="themeText css-125"
data-test="destination-subscription-name"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -2640,11 +2628,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
<span
class="bold themeText css-125"
>
Destination account
Source account
</span>
<span
class="themeText css-125"
data-test="destination-account-name"
data-test="source-account-name"
>
test-account
</span>

View File

@@ -38,12 +38,6 @@ describe("AccountDropdown", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
target: {
subscription: {
subscriptionId: "test-subscription-id",
displayName: "Test Subscription",
@@ -52,7 +46,13 @@ describe("AccountDropdown", () => {
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
target: {
subscriptionId: "",
account: null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
} as CopyJobContextState;
const mockCopyJobContextValue = {
@@ -129,11 +129,11 @@ describe("AccountDropdown", () => {
renderWithContext();
expect(
screen.getByText(`${ContainerCopyMessages.destinationAccountDropdownLabel}:`, { exact: true }),
screen.getByText(`${ContainerCopyMessages.sourceAccountDropdownLabel}:`, { exact: true }),
).toBeInTheDocument();
expect(screen.getByRole("combobox")).toHaveAttribute(
"aria-label",
ContainerCopyMessages.destinationAccountDropdownLabel,
ContainerCopyMessages.sourceAccountDropdownLabel,
);
});
@@ -202,7 +202,7 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.target.account).toEqual({
expect(newState.source.account).toEqual({
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
});
@@ -226,21 +226,20 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.target.account).toEqual({
expect(newState.source.account).toEqual({
...mockDatabaseAccount2,
id: normalizeAccountId(mockDatabaseAccount2.id),
});
});
it("should keep current account if it exists in the filtered list", async () => {
const normalizedAccount1 = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
const contextWithSelectedAccount = {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
target: {
...mockCopyJobState.target,
account: normalizedAccount1,
source: {
...mockCopyJobState.source,
account: mockDatabaseAccount1,
},
},
};
@@ -257,9 +256,12 @@ describe("AccountDropdown", () => {
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
expect(newState).toEqual({
...contextWithSelectedAccount.copyJobState,
target: {
...contextWithSelectedAccount.copyJobState.target,
account: normalizedAccount1,
source: {
...contextWithSelectedAccount.copyJobState.source,
account: {
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
},
},
});
});
@@ -295,8 +297,8 @@ describe("AccountDropdown", () => {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
target: {
...mockCopyJobState.target,
source: {
...mockCopyJobState.source,
account: portalAccount,
},
},
@@ -321,8 +323,8 @@ describe("AccountDropdown", () => {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
target: {
...mockCopyJobState.target,
source: {
...mockCopyJobState.source,
account: hostedAccount,
},
},
@@ -359,8 +361,8 @@ describe("AccountDropdown", () => {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
target: {
...mockCopyJobState.target,
source: {
...mockCopyJobState.source,
subscription: null,
},
} as CopyJobContextState,
@@ -374,13 +376,13 @@ describe("AccountDropdown", () => {
});
it("should not update state if account is already selected and the same", async () => {
const selectedAccount = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
const selectedAccount = mockDatabaseAccount1;
const contextWithSelectedAccount = {
...mockCopyJobContextValue,
copyJobState: {
...mockCopyJobState,
target: {
...mockCopyJobState.target,
source: {
...mockCopyJobState.source,
account: selectedAccount,
},
},
@@ -407,7 +409,7 @@ describe("AccountDropdown", () => {
renderWithContext();
const dropdown = screen.getByRole("combobox");
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.destinationAccountDropdownLabel);
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.sourceAccountDropdownLabel);
});
it("should have required attribute", () => {

View File

@@ -25,7 +25,7 @@ export const normalizeAccountId = (id: string = "") => {
export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const { copyJobState, setCopyJobState } = useCopyJobContext();
const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
const sqlApiOnlyAccounts = (allAccounts || [])
.filter((account) => apiType(account) === "SQL")
@@ -36,11 +36,11 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const updateCopyJobState = (newAccount: DatabaseAccount) => {
setCopyJobState((prevState) => {
if (prevState.target?.account?.id !== newAccount.id) {
if (prevState.source?.account?.id !== newAccount.id) {
return {
...prevState,
target: {
...prevState.target,
source: {
...prevState.source,
account: newAccount,
},
};
@@ -51,13 +51,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
useEffect(() => {
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
const currentAccountId = copyJobState?.target?.account?.id;
const currentAccountId = copyJobState?.source?.account?.id;
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
const selectedAccountId = currentAccountId || predefinedAccountId;
const matchedAccount: DatabaseAccount | null =
const targetAccount: DatabaseAccount | null =
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
updateCopyJobState(matchedAccount || sqlApiOnlyAccounts[0]);
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
}
}, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]);
@@ -77,13 +77,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
};
const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0;
const selectedAccountId = normalizeAccountId(copyJobState?.target?.account?.id ?? "");
const selectedAccountId = normalizeAccountId(copyJobState?.source?.account?.id ?? "");
return (
<FieldRow label={ContainerCopyMessages.destinationAccountDropdownLabel}>
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}>
<Dropdown
placeholder={ContainerCopyMessages.destinationAccountDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.destinationAccountDropdownLabel}
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder}
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel}
options={accountOptions}
disabled={isAccountDropdownDisabled}
required

View File

@@ -29,7 +29,7 @@ describe("MigrationType", () => {
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
},
setCopyJobState: mockSetCopyJobState,
flow: { currentScreen: "selectAccount" },

View File

@@ -17,11 +17,11 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
const updateCopyJobState = (newSubscription: Subscription) => {
setCopyJobState((prevState) => {
if (prevState.target?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
if (prevState.source?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
return {
...prevState,
target: {
...prevState.target,
source: {
...prevState.source,
subscription: newSubscription,
account: null,
},
@@ -33,7 +33,7 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
useEffect(() => {
if (subscriptions && subscriptions.length > 0) {
const currentSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
const currentSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const predefinedSubscriptionId = userContext.subscriptionId;
const selectedSubscriptionId = currentSubscriptionId || predefinedSubscriptionId;
@@ -61,7 +61,7 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
}
};
const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
return (
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}>

View File

@@ -30,18 +30,18 @@ describe("SelectAccount", () => {
jobName: "",
migrationType: CopyJobMigrationType.Online,
source: {
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
sourceReadWriteAccessFromTarget: false,
target: {
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
},
setCopyJobState: mockSetCopyJobState,
flow: { currentScreen: "selectAccount" },
@@ -68,7 +68,7 @@ describe("SelectAccount", () => {
expect(container.firstChild).toHaveAttribute("data-test", "Panel:SelectAccountContainer");
expect(container.firstChild).toHaveClass("selectAccountContainer");
expect(screen.getByText(/Please select a destination account to copy to/i)).toBeInTheDocument();
expect(screen.getByText(/Please select a source account from which to copy/i)).toBeInTheDocument();
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();

View File

@@ -8,7 +8,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
<span
class="themeText css-110"
>
Please select a destination account to copy to.
Please select a source account from which to copy.
</span>
<div
data-testid="subscription-dropdown"

View File

@@ -7,9 +7,19 @@ import { dropDownChangeHandler } from "./DropDownChangeHandler";
const createMockInitialState = (): CopyJobContextState => ({
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
source: {
subscriptionId: "source-sub-id",
subscription: {
subscriptionId: "source-sub-id",
displayName: "Source Subscription",
state: "Enabled",
subscriptionPolicies: {
locationPlacementId: "test",
quotaId: "test",
spendingLimit: "Off",
},
authorizationSource: "test",
},
account: {
id: "source-account-id",
name: "source-account",
@@ -40,17 +50,7 @@ const createMockInitialState = (): CopyJobContextState => ({
containerId: "source-container",
},
target: {
subscription: {
subscriptionId: "target-sub-id",
displayName: "Target Subscription",
state: "Enabled",
subscriptionPolicies: {
locationPlacementId: "test",
quotaId: "test",
spendingLimit: "Off",
},
authorizationSource: "test",
},
subscriptionId: "target-sub-id",
account: {
id: "target-account-id",
name: "target-account",
@@ -169,7 +169,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.source.databaseId).toBe("new-source-db");
expect(capturedState.source.containerId).toBeUndefined();
expect(capturedState.source.subscriptionId).toEqual(initialState.source.subscriptionId);
expect(capturedState.source.subscription).toEqual(initialState.source.subscription);
expect(capturedState.source.account).toEqual(initialState.source.account);
expect(capturedState.target).toEqual(initialState.target);
});
@@ -181,7 +181,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget);
});
});
@@ -193,7 +193,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.source.containerId).toBe("new-source-container");
expect(capturedState.source.databaseId).toBe(initialState.source.databaseId);
expect(capturedState.source.subscriptionId).toEqual(initialState.source.subscriptionId);
expect(capturedState.source.subscription).toEqual(initialState.source.subscription);
expect(capturedState.source.account).toEqual(initialState.source.account);
expect(capturedState.target).toEqual(initialState.target);
});
@@ -215,7 +215,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.target.databaseId).toBe("new-target-db");
expect(capturedState.target.containerId).toBeUndefined();
expect(capturedState.target.subscription).toEqual(initialState.target.subscription);
expect(capturedState.target.subscriptionId).toBe(initialState.target.subscriptionId);
expect(capturedState.target.account).toEqual(initialState.target.account);
expect(capturedState.source).toEqual(initialState.source);
});
@@ -227,7 +227,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget);
});
});
@@ -239,7 +239,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.target.containerId).toBe("new-target-container");
expect(capturedState.target.databaseId).toBe(initialState.target.databaseId);
expect(capturedState.target.subscription).toEqual(initialState.target.subscription);
expect(capturedState.target.subscriptionId).toBe(initialState.target.subscriptionId);
expect(capturedState.target.account).toEqual(initialState.target.account);
expect(capturedState.source).toEqual(initialState.source);
});

View File

@@ -73,7 +73,7 @@ describe("SelectSourceAndTargetContainers", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "test-subscription-id",
subscription: { subscriptionId: "test-subscription-id" },
account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
@@ -82,7 +82,7 @@ describe("SelectSourceAndTargetContainers", () => {
containerId: "container1",
},
target: {
subscription: { subscriptionId: "test-subscription-id" },
subscriptionId: "test-subscription-id",
account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
@@ -90,7 +90,7 @@ describe("SelectSourceAndTargetContainers", () => {
databaseId: "db2",
containerId: "container2",
},
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
};
const mockMemoizedData = {

View File

@@ -69,15 +69,15 @@ describe("useSourceAndTargetData", () => {
const mockCopyJobState: CopyJobContextState = {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
sourceReadWriteAccessFromTarget: false,
sourceReadAccessFromTarget: false,
source: {
subscriptionId: "source-subscription-id",
subscription: mockSubscription,
account: mockSourceAccount,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: mockSubscription,
subscriptionId: "target-subscription-id",
account: mockTargetAccount,
databaseId: "target-db",
containerId: "target-container",

View File

@@ -86,13 +86,13 @@ describe("useCopyJobNavigation", () => {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "source-sub-id",
subscription: { subscriptionId: "source-sub-id" } as any,
account: { id: "source-account-id", name: "Account-1" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: { subscriptionId: "target-sub-id" } as any,
subscriptionId: "target-sub-id",
account: { id: "target-account-id", name: "Account-2" } as any,
databaseId: "target-db",
containerId: "target-container",

View File

@@ -142,14 +142,14 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "test-sub",
subscription: { subscriptionId: "test-sub" } as any,
account: { name: "test-account" } as any,
databaseId: "",
containerId: "",
},
target: {
subscription: { subscriptionId: "test-sub" } as any,
account: { name: "test-account" } as any,
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
@@ -171,14 +171,14 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "",
account: null as any,
subscription: null as any,
account: { name: "test-account" } as any,
databaseId: "",
containerId: "",
},
target: {
subscription: null as any,
account: { name: "test-account" } as any,
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
},
@@ -210,13 +210,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "",
subscription: null as any,
account: null as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscription: null as any,
subscriptionId: "",
account: null as any,
databaseId: "target-db",
containerId: "target-container",
@@ -240,13 +240,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "",
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "source-container",
},
target: {
subscription: null as any,
subscriptionId: "",
account: null as any,
databaseId: "target-db",
containerId: "target-container",
@@ -288,13 +288,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "valid-job-name_123",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "",
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscription: null as any,
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
@@ -318,13 +318,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "invalid job name with spaces!",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "",
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscription: null as any,
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",
@@ -348,13 +348,13 @@ describe("useCreateCopyJobScreensList", () => {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscriptionId: "",
subscription: null as any,
account: null as any,
databaseId: "",
containerId: "",
},
target: {
subscription: null as any,
subscriptionId: "",
account: null as any,
databaseId: "",
containerId: "",

View File

@@ -36,7 +36,7 @@ function useCreateCopyJobScreensList(goBack: () => void): Screen[] {
component: <SelectAccount />,
validations: [
{
validate: (state: CopyJobContextState) => !!state?.target?.subscription && !!state?.target?.account,
validate: (state: CopyJobContextState) => !!state?.source?.subscription && !!state?.source?.account,
message: "Please select a subscription and account to proceed",
},
],

View File

@@ -19,7 +19,7 @@ jest.mock("../../ContainerCopyMessages", () => ({
sourceContainerLabel: "Source Container",
targetDatabaseLabel: "Destination Database",
targetContainerLabel: "Destination Container",
destinationAccountLabel: "Destination account",
sourceAccountLabel: "Source Account",
MonitorJobs: {
Columns: {
lastUpdatedTime: "Date & time",
@@ -102,8 +102,8 @@ describe("CopyJobDetails", () => {
expect(screen.getByText("Date & time")).toBeInTheDocument();
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
expect(screen.getByText("Destination account")).toBeInTheDocument();
expect(screen.getByText("targetAccount")).toBeInTheDocument();
expect(screen.getByText("Source Account")).toBeInTheDocument();
expect(screen.getByText("sourceAccount")).toBeInTheDocument();
expect(screen.getByText("Mode")).toBeInTheDocument();
expect(screen.getByText("Offline")).toBeInTheDocument();
@@ -263,7 +263,7 @@ describe("CopyJobDetails", () => {
expect(screen.getByText("complex_source_container_with_underscores")).toBeInTheDocument();
expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument();
expect(screen.getByText("complex_target_container_with_underscores")).toBeInTheDocument();
expect(screen.getByText("complex.target.account")).toBeInTheDocument();
expect(screen.getByText("complex.source.account")).toBeInTheDocument();
});
});
@@ -322,11 +322,11 @@ describe("CopyJobDetails", () => {
render(<CopyJobDetails job={mockBasicJob} />);
const dateTimeHeading = screen.getByText("Date & time");
const destinationAccountHeading = screen.getByText("Destination account");
const sourceAccountHeading = screen.getByText("Source Account");
const modeHeading = screen.getByText("Mode");
expect(dateTimeHeading).toHaveClass("bold");
expect(destinationAccountHeading).toHaveClass("bold");
expect(sourceAccountHeading).toHaveClass("bold");
expect(modeHeading).toHaveClass("bold");
});
});

View File

@@ -106,8 +106,8 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
<Text className="themeText">{job.LastUpdatedTime}</Text>
</Stack.Item>
<Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold themeText">{ContainerCopyMessages.destinationAccountLabel}</Text>
<Text className="themeText">{job.Destination?.remoteAccountName}</Text>
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
</Stack.Item>
<Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>

View File

@@ -55,15 +55,15 @@ export interface DatabaseContainerSectionProps {
export interface CopyJobContextState {
jobName: string;
migrationType: CopyJobMigrationType;
sourceReadWriteAccessFromTarget?: boolean;
sourceReadAccessFromTarget?: boolean;
source: {
subscriptionId: string;
subscription: Subscription | null;
account: DatabaseAccount | null;
databaseId: string;
containerId: string;
};
target: {
subscription: Subscription | null;
subscriptionId: string;
account: DatabaseAccount | null;
databaseId: string;
containerId: string;

View File

@@ -7,7 +7,6 @@ import {
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { useDatabases } from "Explorer/useDatabases";
import { Keys, t } from "Localization";
import { isFabric, isFabricNative, openRestoreContainerDialog } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -24,9 +23,7 @@ import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import PinIcon from "../../images/Pin.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel";
@@ -38,6 +35,7 @@ import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import { useSelectedNode } from "./useSelectedNode";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
export interface CollectionContextMenuButtonParams {
databaseId: string;
@@ -54,18 +52,12 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
if (isFabric() && userContext.fabricContext?.isReadOnly) {
return undefined;
}
const isPinned = useDatabases.getState().isPinned(databaseId);
const items: TreeNodeMenuItem[] = [
{
iconSrc: PinIcon,
onClick: () => useDatabases.getState().togglePinDatabase(databaseId),
label: isPinned ? "Unpin from top" : "Pin to top",
},
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked({ databaseId }),
label: t(Keys.contextMenu.newContainer, { containerName: getCollectionName() }),
label: `New ${getCollectionName()}`,
},
];
@@ -75,7 +67,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
items.push({
iconSrc: AddCollectionIcon,
onClick: () => openRestoreContainerDialog(),
label: t(Keys.contextMenu.restoreContainer, { containerName: getCollectionName() }),
label: `Restore ${getCollectionName()}`,
});
}
}
@@ -84,15 +76,15 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
useSidePanel.getState().getRef = lastFocusedElement;
useSidePanel
.getState()
.openSidePanel(
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
);
(useSidePanel.getState().getRef = lastFocusedElement),
useSidePanel
.getState()
.openSidePanel(
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
);
},
label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
});
}
@@ -108,7 +100,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
label: t(Keys.contextMenu.newSqlQuery),
label: "New SQL Query",
});
}
@@ -116,7 +108,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
label: t(Keys.contextMenu.newQuery),
label: "New Query",
});
items.push({
@@ -131,8 +123,8 @@ export const createCollectionContextMenuButton = (
},
label:
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
? t(Keys.contextMenu.openMongoShell)
: t(Keys.contextMenu.newShell),
? "Open Mongo Shell"
: "New Shell",
});
}
@@ -145,7 +137,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
},
label: t(Keys.contextMenu.openCassandraShell),
label: "Open Cassandra Shell",
});
}
@@ -158,7 +150,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: t(Keys.contextMenu.newStoredProcedure),
label: "New Stored Procedure",
});
items.push({
@@ -166,7 +158,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
label: t(Keys.contextMenu.newUdf),
label: "New UDF",
});
items.push({
@@ -174,7 +166,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: t(Keys.contextMenu.newTrigger),
label: "New Trigger",
});
}
@@ -183,15 +175,15 @@ export const createCollectionContextMenuButton = (
iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
useSelectedNode.getState().setSelectedNode(selectedCollection);
useSidePanel.getState().getRef = lastFocusedElement;
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
);
(useSidePanel.getState().getRef = lastFocusedElement),
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
);
},
label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
}
@@ -228,14 +220,14 @@ export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] =>
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
},
label: t(Keys.contextMenu.newSqlQuery),
label: "New SQL Query",
});
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
label: t(Keys.contextMenu.newSqlQuery),
label: "New SQL Query",
});
}
}
@@ -255,7 +247,7 @@ export const createStoreProcedureContextMenuItems = (
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: t(Keys.contextMenu.deleteStoredProcedure),
label: "Delete Stored Procedure",
},
];
};
@@ -269,7 +261,7 @@ export const createTriggerContextMenuItems = (container: Explorer, trigger: Trig
{
iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(),
label: t(Keys.contextMenu.deleteTrigger),
label: "Delete Trigger",
},
];
};
@@ -286,7 +278,7 @@ export const createUserDefinedFunctionContextMenuItems = (
{
iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(),
label: t(Keys.contextMenu.deleteUdf),
label: "Delete User Defined Function",
},
];
};

View File

@@ -58,11 +58,6 @@ export interface CommandButtonComponentProps {
*/
tooltipText?: string;
/**
* Rich JSX content for tooltip (used instead of tooltipText when provided)
*/
tooltipContent?: React.ReactNode;
/**
* Custom styles to apply to the button using Fluent UI theme tokens
*/

View File

@@ -17,7 +17,6 @@ import {
} from "@fluentui/react";
import React, { FC, useEffect } from "react";
import create, { UseStore } from "zustand";
import { Keys, t } from "Localization";
export interface DialogState {
visible: boolean;
@@ -89,7 +88,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
isModal: true,
title,
subText,
primaryButtonText: t(Keys.common.close),
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
get().closeDialog();

View File

@@ -44,7 +44,6 @@ import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { Keys, t } from "Localization";
import {
ConflictResolutionComponent,
ConflictResolutionComponentProps,
@@ -690,12 +689,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => {
const validationErrors = [];
if (newDataMasking.includedPaths === undefined || newDataMasking.includedPaths === null) {
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsRequired));
validationErrors.push("includedPaths is required");
} else if (!Array.isArray(newDataMasking.includedPaths)) {
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsMustBeArray));
validationErrors.push("includedPaths must be an array");
}
if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) {
validationErrors.push(t(Keys.controls.settings.dataMasking.excludedPathsMustBeArray));
validationErrors.push("excludedPaths must be an array if provided");
}
this.setState({
@@ -897,7 +896,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const buttons: CommandButtonComponentProps[] = [];
const isExecuting = this.props.settingsTab.isExecuting();
if (this.saveSettingsButton.isVisible()) {
const label = t(Keys.common.save);
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
@@ -910,7 +909,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
if (this.discardSettingsChangesButton.isVisible()) {
const label = t(Keys.common.discard);
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
@@ -935,10 +934,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
throughputCap: String(throughputCap),
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
});
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
}
this.setState({ autoPilotThroughput: newThroughput, throughputError });
};
@@ -949,10 +947,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
throughputCap: String(throughputCap),
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
});
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
}
this.setState({ throughput: newThroughput, throughputError });
};
@@ -1563,7 +1560,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
>
{this.shouldShowKeyspaceSharedThroughputMessage() && (
<div>{t(Keys.controls.settings.scale.keyspaceSharedThroughput)}</div>
<div>This table shared throughput is configured at the keyspace</div>
)}
<div

View File

@@ -22,7 +22,6 @@ import {
Stack,
Text,
} from "@fluentui/react";
import { Keys, t } from "Localization";
import * as React from "react";
import { Urls } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
@@ -339,12 +338,10 @@ export const getEstimatedSpendingElement = (
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
return (
<Stack>
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.costEstimate.title)}
</Text>
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>Cost estimate*</Text>
{costElement}
<Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.costEstimate.howWeCalculate)}
How we calculate this
</Text>
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
<span>
@@ -356,8 +353,7 @@ export const getEstimatedSpendingElement = (
</span>
<span>
{priceBreakdown.currencySign}
{priceBreakdown.pricePerRu}
{t(Keys.controls.settings.costEstimate.perRu)}
{priceBreakdown.pricePerRu}/RU
</span>
</Stack>
<Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
@@ -369,16 +365,18 @@ export const getEstimatedSpendingElement = (
export const manualToAutoscaleDisclaimerElement: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
{t(Keys.controls.settings.throughput.manualToAutoscaleDisclaimer)}{" "}
<Link href={Urls.autoscaleMigration}>{t(Keys.common.learnMore)}</Link>
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
<Link href={Urls.autoscaleMigration}>Learn more</Link>
</Text>
);
export const ttlWarning: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.throughput.ttlWarningText)}{" "}
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete
operation explicitly issued by a client application. For more information see,{" "}
<Link target="_blank" href="https://aka.ms/cosmos-db-ttl">
{t(Keys.controls.settings.throughput.ttlWarningLinkText)}
Time to Live (TTL) in Azure Cosmos DB
</Link>
.
</Text>
@@ -386,28 +384,29 @@ export const ttlWarning: JSX.Element = (
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.throughput.unsavedEditorWarningPrefix)}{" "}
You have not saved the latest changes made to your{" "}
{editor === "indexPolicy"
? t(Keys.controls.settings.throughput.unsavedIndexingPolicy)
? "indexing policy"
: editor === "dataMasking"
? t(Keys.controls.settings.throughput.unsavedDataMaskingPolicy)
: t(Keys.controls.settings.throughput.unsavedComputedProperties)}
{t(Keys.controls.settings.throughput.unsavedEditorWarningSuffix)}
? "data masking policy"
: "computed properties"}
. Please click save to confirm the changes.
</Text>
);
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
{t(Keys.controls.settings.throughput.updateDelayedApplyWarning)}
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
time to complete.
</Text>
);
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
return (
<Text id="updateThroughputDelayedApplyWarningMessage">
{t(Keys.controls.settings.throughput.scalingUpDelayMessage, {
instantMaximumThroughput: String(instantMaximumThroughput),
})}
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
with this value and wait until the scale-up is completed.
</Text>
);
};
@@ -419,26 +418,22 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
return (
<>
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
{t(Keys.controls.settings.throughput.exceedPreAllocatedMessage)}
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
There are three options you can choose from to proceed:
</Text>
<ol style={{ fontSize: 14, color: "var(--colorNeutralForeground1)", marginTop: "5px" }}>
<li>
{t(Keys.controls.settings.throughput.instantScaleOption, {
instantMaximumThroughput: String(instantMaximumThroughput),
})}
</li>
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
{instantMaximumThroughput < maximumThroughput && (
<li>
{t(Keys.controls.settings.throughput.asyncScaleOption, { maximumThroughput: String(maximumThroughput) })}
</li>
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
)}
<li>
{t(Keys.controls.settings.throughput.quotaMaxOption, { maximumThroughput: String(maximumThroughput) })}
Your current quota max is {maximumThroughput} RU/s. To go over this limit, you must request a quota increase
and the Azure Cosmos DB team will review.
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
target="_blank"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</li>
</ol>
@@ -449,19 +444,23 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
return (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.throughput.belowMinimumMessage, { minimum: String(minimum) })}
You are not able to lower throughput below your current minimum of {minimum} RU/s. For more information on this
limit, please refer to our service quote documentation.
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
);
};
export const saveThroughputWarningMessage: JSX.Element = (
<Text>{t(Keys.controls.settings.throughput.saveThroughputWarning)}</Text>
<Text>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
before saving your changes
</Text>
);
const getCurrentThroughput = (
@@ -473,29 +472,23 @@ const getCurrentThroughput = (
if (targetThroughput) {
if (throughput) {
return isAutoscale
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
? `, Current autoscale throughput: ${Math.round(
throughput / 10,
)} - ${throughput} ${throughputUnit}, ${t(
Keys.controls.settings.throughput.targetAutoscaleThroughput,
)} ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}, ${t(
Keys.controls.settings.throughput.targetManualThroughput,
)} ${targetThroughput}`;
} else {
return isAutoscale
? `, ${t(Keys.controls.settings.throughput.targetAutoscaleThroughput)} ${Math.round(
)} - ${throughput} ${throughputUnit}, Target autoscale throughput: ${Math.round(
targetThroughput / 10,
)} - ${targetThroughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.targetManualThroughput)} ${targetThroughput} ${throughputUnit}`;
: `, Current manual throughput: ${throughput} ${throughputUnit}, Target manual throughput: ${targetThroughput}`;
} else {
return isAutoscale
? `, Target autoscale throughput: ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
: `, Target manual throughput: ${targetThroughput} ${throughputUnit}`;
}
}
if (!targetThroughput && throughput) {
return isAutoscale
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
throughput / 10,
)} - ${throughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}`;
? `, Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} ${throughputUnit}`
: `, Current manual throughput: ${throughput} ${throughputUnit}`;
}
return "";
@@ -510,10 +503,10 @@ export const getThroughputApplyDelayedMessage = (
requestedThroughput: number,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.throughput.applyDelayedMessage)}
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days
to complete. View the latest status in Notifications.
<br />
{t(Keys.controls.settings.throughput.databaseLabel)} {databaseName},{" "}
{t(Keys.controls.settings.throughput.containerLabel)} {collectionName}{" "}
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);
@@ -526,13 +519,9 @@ export const getThroughputApplyShortDelayMessage = (
collectionName: string,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
{t(Keys.controls.settings.throughput.applyShortDelayMessage)}
A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br />
{collectionName
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
Keys.controls.settings.throughput.containerLabel,
)} ${collectionName} `
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
</Text>
);
@@ -546,13 +535,10 @@ export const getThroughputApplyLongDelayMessage = (
requestedThroughput: number,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage">
{t(Keys.controls.settings.throughput.applyLongDelayMessage)}
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
complete. View the latest status in Notifications.
<br />
{collectionName
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
Keys.controls.settings.throughput.containerLabel,
)} ${collectionName} `
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);
@@ -561,49 +547,63 @@ export const getToolTipContainer = (content: string | JSX.Element): JSX.Element
content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined;
export const conflictResolutionLwwTooltip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.conflictResolution.lwwTooltip)}</Text>
<Text styles={infoAndToolTipTextStyle}>
Gets or sets the name of a integer property in your documents which is used for the Last Write Wins (LWW) based
conflict resolution scheme. By default, the system uses the system defined timestamp property, _ts to decide the
winner for the conflicting versions of the document. Specify your own integer property if you want to override the
default timestamp based conflict resolution.
</Text>
);
export const conflictResolutionCustomToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.conflictResolution.customTooltip)}
Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write
application defined logic to determine the winner of the conflicting versions of a document. The stored procedure
will get executed transactionally, exactly once, on the server side. If you do not provide a stored procedure, the
conflicts will be populated in the
<Link className="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank">
{t(Keys.controls.settings.conflictResolution.customTooltipConflictsFeed)}
{` conflicts feed`}
</Link>
{t(Keys.controls.settings.conflictResolution.customTooltipSuffix)}
. You can update/re-register the stored procedure at any time.
</Text>
);
export const changeFeedPolicyToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.changeFeed.tooltip)}</Text>
<Text styles={infoAndToolTipTextStyle}>
Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default.
To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes.
Reads are unaffected.
</Text>
);
export const mongoIndexingPolicyDisclaimer: JSX.Element = (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.mongoIndexing.disclaimer)}
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.
<Link
href="https://docs.microsoft.com/azure/cosmos-db/mongodb-indexing#index-types"
target="_blank"
style={{ color: "var(--colorBrandForeground1)" }}
>
{t(Keys.controls.settings.mongoIndexing.disclaimerCompoundIndexesLink)}
{` Compound indexes `}
</Link>
{t(Keys.controls.settings.mongoIndexing.disclaimerSuffix)}
are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo
shell.
</Text>
);
export const mongoCompoundIndexNotSupportedMessage: JSX.Element = (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.mongoIndexing.compoundNotSupported)}
Collections with compound indexes are not yet supported in the indexing editor. To modify indexing policy for this
collection, use the Mongo Shell.
</Text>
);
export const mongoIndexingPolicyAADError: JSX.Element = (
<MessageBar messageBarType={MessageBarType.error}>
<Text>
{t(Keys.controls.settings.mongoIndexing.aadError)}
To use the indexing policy editor, please login to the
<Link target="_blank" href="https://portal.azure.com">
{t(Keys.controls.settings.mongoIndexing.aadErrorLink)}
{"azure portal."}
</Link>
</Text>
</MessageBar>
@@ -611,7 +611,7 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
<Stack horizontal {...mongoWarningStackProps}>
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.mongoIndexing.refreshingProgress)}</Text>
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
<Spinner size={SpinnerSize.small} />
</Stack>
);
@@ -623,18 +623,15 @@ export const renderMongoIndexTransformationRefreshMessage = (
if (progress === 0) {
return (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesZero)}
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheck)}</Link>
{"You can make more indexing changes once the current index transformation is complete. "}
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
</Text>
);
} else {
return (
<Text styles={infoAndToolTipTextStyle}>
{`${t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesProgress).replace(
"{{progress}}",
String(progress),
)} `}
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheckProgress)}</Link>
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `}
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link>
</Text>
);
}

View File

@@ -4,7 +4,6 @@ import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/C
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { loadMonaco } from "Explorer/LazyMonaco";
import { monacoTheme, useThemeStore } from "hooks/useTheme";
import { Keys, t } from "Localization";
import * as monaco from "monaco-editor";
import * as React from "react";
export interface ComputedPropertiesComponentProps {
@@ -108,7 +107,7 @@ export class ComputedPropertiesComponent extends React.Component<
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
value: value,
language: "json",
ariaLabel: t(Keys.controls.settings.computedProperties.ariaLabel),
ariaLabel: "Computed properties",
theme: monacoTheme(),
});
if (this.computedPropertiesEditor) {
@@ -152,9 +151,9 @@ export class ComputedPropertiesComponent extends React.Component<
)}
<Text style={{ marginLeft: "30px", marginBottom: "10px", color: "var(--colorNeutralForeground1)" }}>
<Link target="_blank" href="https://aka.ms/computed-properties-preview/">
{t(Keys.common.learnMore)} <FontIcon iconName="NavigateExternalInline" />
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
</Link>
&#160; {t(Keys.controls.settings.computedProperties.learnMorePrefix)}
&#160; about how to define computed properties and how to use them.
</Text>
<div
className="settingsV2Editor"

View File

@@ -2,7 +2,6 @@ import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } fr
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import {
conflictResolutionCustomToolTip,
conflictResolutionLwwTooltip,
@@ -33,12 +32,9 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
{
key: DataModels.ConflictResolutionMode.LastWriterWins,
text: t(Keys.controls.settings.conflictResolution.lwwDefault),
},
{
key: DataModels.ConflictResolutionMode.Custom,
text: t(Keys.controls.settings.conflictResolution.customMergeProcedure),
text: "Last Write Wins (default)",
},
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" },
];
componentDidMount(): void {
@@ -89,7 +85,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private getConflictResolutionModeComponent = (): JSX.Element => (
<ChoiceGroup
label={t(Keys.controls.settings.conflictResolution.mode)}
label="Mode"
selectedKey={this.props.conflictResolutionPolicyMode}
options={this.conflictResolutionChoiceGroupOptions}
onChange={this.onConflictResolutionPolicyModeChange}
@@ -107,7 +103,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private getConflictResolutionLWWComponent = (): JSX.Element => (
<TextField
id="conflictResolutionLwwTextField"
label={t(Keys.controls.settings.conflictResolution.conflictResolverProperty)}
label={"Conflict Resolver Property"}
onRenderLabel={this.onRenderLwwComponentTextField}
styles={{
fieldGroup: {
@@ -162,7 +158,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
return (
<TextField
id="conflictResolutionCustomTextField"
label={t(Keys.controls.settings.conflictResolution.storedProcedure)}
label="Stored procedure"
onRenderLabel={this.onRenderCustomComponentTextField}
styles={{
fieldGroup: {

View File

@@ -7,7 +7,6 @@ import {
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { Keys, t } from "Localization";
import React from "react";
export interface ContainerPolicyComponentProps {
@@ -154,7 +153,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
<PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
headerText={t(Keys.controls.settings.containerPolicy.vectorPolicy)}
headerText="Vector Policy"
>
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{vectorEmbeddings && (
@@ -176,7 +175,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
<PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
headerText={t(Keys.controls.settings.containerPolicy.fullTextPolicy)}
headerText="Full Text Policy"
>
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{fullTextSearchPolicy ? (
@@ -219,7 +218,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
});
}}
>
{t(Keys.controls.settings.containerPolicy.createFullTextPolicy)}
Create new full text search policy
</DefaultButton>
)}
</Stack>

View File

@@ -2,7 +2,6 @@ import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
import * as monaco from "monaco-editor";
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import { Keys, t } from "Localization";
import { loadMonaco } from "../../../LazyMonaco";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { isDirty as isContentDirty, isDataMaskingEnabled } from "../SettingsUtils";
@@ -90,7 +89,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
value: value,
language: "json",
automaticLayout: true,
ariaLabel: t(Keys.controls.settings.dataMasking.ariaLabel),
ariaLabel: "Data Masking Policy",
fontSize: 13,
minimap: { enabled: false },
wordWrap: "off",
@@ -143,7 +142,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
)}
{this.props.validationErrors.length > 0 && (
<MessageBar messageBarType={MessageBarType.error}>
{t(Keys.controls.settings.dataMasking.validationFailed)} {this.props.validationErrors.join(", ")}
Validation failed: {this.props.validationErrors.join(", ")}
</MessageBar>
)}
<div className="settingsV2Editor" tabIndex={0} ref={this.dataMaskingDiv}></div>

View File

@@ -2,7 +2,6 @@ import { FontIcon, Link, Stack, Text } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
@@ -22,9 +21,7 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
{isSourceContainer && (
<Text styles={{ root: { fontWeight: 600 } }}>
{t(Keys.controls.settings.globalSecondaryIndex.indexesDefined)}
</Text>
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes defined for it.</Text>
)}
<Text>
<Link
@@ -34,7 +31,7 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
Learn more
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
</Link>{" "}
{t(Keys.controls.settings.globalSecondaryIndex.learnMoreSuffix)}
about how to define global secondary indexes and how to use them.
</Text>
</Stack>
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}

View File

@@ -9,7 +9,6 @@ import { useSidePanel } from "hooks/useSidePanel";
import * as monaco from "monaco-editor";
import React, { useEffect, useRef } from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
export interface GlobalSecondaryIndexSourceComponentProps {
collection: ViewModels.Collection;
@@ -68,7 +67,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
value: jsonValue,
language: "json",
ariaLabel: t(Keys.controls.settings.globalSecondaryIndex.jsonAriaLabel),
ariaLabel: "Global Secondary Index JSON",
readOnly: true,
});
};
@@ -99,7 +98,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
}}
/>
<PrimaryButton
text={t(Keys.controls.settings.globalSecondaryIndex.addIndex)}
text="Add index"
styles={{ root: { width: "fit-content", marginTop: 12 } }}
onClick={() =>
useSidePanel

View File

@@ -1,7 +1,6 @@
import { Stack, Text } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
export interface GlobalSecondaryIndexTargetComponentProps {
collection: ViewModels.Collection;
@@ -26,21 +25,17 @@ export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexT
return (
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
<Text styles={textHeadingStyle}>{t(Keys.controls.settings.globalSecondaryIndex.settingsTitle)}</Text>
<Text styles={textHeadingStyle}>Global Secondary Index Settings</Text>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>
{t(Keys.controls.settings.globalSecondaryIndex.sourceContainer)}
</Text>
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
</Stack>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>
{t(Keys.controls.settings.globalSecondaryIndex.indexDefinition)}
</Text>
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
</Stack>

View File

@@ -3,7 +3,6 @@ import { monacoTheme, useThemeStore } from "hooks/useTheme";
import * as monaco from "monaco-editor";
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import { Keys, t } from "Localization";
import { loadMonaco } from "../../../LazyMonaco";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { isDirty, isIndexTransforming } from "../SettingsUtils";
@@ -120,7 +119,7 @@ export class IndexingPolicyComponent extends React.Component<
value: value,
language: "json",
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
ariaLabel: t(Keys.controls.settings.indexingPolicy.ariaLabel),
ariaLabel: "Indexing Policy",
theme: monacoTheme(),
});
if (this.indexingPolicyEditor) {

View File

@@ -1,7 +1,6 @@
import { MessageBar, MessageBarType } from "@fluentui/react";
import * as React from "react";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { Keys, t } from "Localization";
import {
mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage,
@@ -47,11 +46,7 @@ export class IndexingPolicyRefreshComponent extends React.Component<
try {
await this.props.refreshIndexTransformationProgress();
} catch (error) {
handleError(
error,
"RefreshIndexTransformationProgress",
t(Keys.controls.settings.indexingPolicyRefresh.refreshFailed),
);
handleError(error, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed");
} finally {
this.setState({ isRefreshing: false });
}

View File

@@ -14,7 +14,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}
>

View File

@@ -9,7 +9,6 @@ import {
IDropdownOption,
ITextField,
} from "@fluentui/react";
import { Keys, t } from "Localization";
import {
addMongoIndexSubElementsTokens,
mongoErrorMessageStyles,
@@ -67,7 +66,7 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
<Stack {...mongoWarningStackProps}>
<Stack horizontal tokens={addMongoIndexSubElementsTokens}>
<TextField
ariaLabel={t(Keys.controls.settings.mongoIndexing.indexFieldName) + " " + this.props.position}
ariaLabel={"Index Field Name " + this.props.position}
disabled={this.props.disabled}
styles={shortWidthTextFieldStyles}
componentRef={this.setRef}
@@ -77,17 +76,17 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
/>
<Dropdown
ariaLabel={t(Keys.controls.settings.mongoIndexing.indexType) + " " + this.props.position}
ariaLabel={"Index Type " + this.props.position}
disabled={this.props.disabled}
styles={shortWidthDropDownStyles}
placeholder={t(Keys.controls.settings.mongoIndexing.selectIndexType)}
placeholder="Select an index type"
selectedKey={this.props.type}
options={this.indexTypes}
onChange={this.onTypeChange}
/>
<IconButton
ariaLabel={t(Keys.controls.settings.mongoIndexing.undoButton) + " " + this.props.position}
ariaLabel={"Undo Button " + this.props.position}
iconProps={{ iconName: "Undo" }}
disabled={!this.props.description && !this.props.type}
onClick={() => this.props.onDiscard()}

View File

@@ -15,7 +15,6 @@ import {
} from "@fluentui/react";
import * as React from "react";
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/cosmos/types";
import { Keys, t } from "Localization";
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
import {
addMongoIndexStackProps,
@@ -84,25 +83,11 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
};
private initialIndexesColumns: IColumn[] = [
{
key: "definition",
name: t(Keys.controls.settings.mongoIndexing.definitionColumn),
fieldName: "definition",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{
key: "type",
name: t(Keys.controls.settings.mongoIndexing.typeColumn),
fieldName: "type",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{ key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true },
{
key: "actionButton",
name: t(Keys.controls.settings.mongoIndexing.dropIndexColumn),
name: "Drop Index",
fieldName: "actionButton",
minWidth: 100,
maxWidth: 200,
@@ -111,25 +96,11 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
];
private indexesToBeDroppedColumns: IColumn[] = [
{
key: "definition",
name: t(Keys.controls.settings.mongoIndexing.definitionColumn),
fieldName: "definition",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{
key: "type",
name: t(Keys.controls.settings.mongoIndexing.typeColumn),
fieldName: "type",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{ key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true },
{
key: "actionButton",
name: t(Keys.controls.settings.mongoIndexing.addIndexBackColumn),
name: "Add index back",
fieldName: "actionButton",
minWidth: 100,
maxWidth: 200,
@@ -190,7 +161,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
return isCurrentIndex ? (
<IconButton
ariaLabel={t(Keys.controls.settings.mongoIndexing.deleteIndexButton)}
ariaLabel="Delete index Button"
iconProps={{ iconName: "Delete" }}
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
onClick={() => {
@@ -199,7 +170,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
/>
) : (
<IconButton
ariaLabel={t(Keys.controls.settings.mongoIndexing.addBackIndexButton)}
ariaLabel="Add back Index Button"
iconProps={{ iconName: "Add" }}
onClick={() => {
this.props.onRevertIndexDrop(arrayPosition);
@@ -287,10 +258,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return (
<Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent
title={t(Keys.controls.settings.mongoIndexing.currentIndexes)}
isExpandedByDefault={true}
>
<CollapsibleSectionComponent title="Current index(es)" isExpandedByDefault={true}>
{
<>
<DetailsList
@@ -317,10 +285,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return (
<Stack styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent
title={t(Keys.controls.settings.mongoIndexing.indexesToBeDropped)}
isExpandedByDefault={true}
>
<CollapsibleSectionComponent title="Index(es) to be dropped" isExpandedByDefault={true}>
{indexesToBeDropped.length > 0 && (
<DetailsList
styles={customDetailsListStyles}

View File

@@ -18,7 +18,6 @@ import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/da
import { Platform, configContext } from "ConfigContext";
import Explorer from "Explorer/Explorer";
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
import { Keys, t } from "Localization";
import {
CosmosSqlDataTransferDataSourceSink,
DataTransferJobGetResults,
@@ -81,7 +80,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
};
const partitionKeyName = t(Keys.controls.settings.partitionKey.partitionKey);
const partitionKeyName = "Partition key";
const partitionKeyValue = getPartitionKeyValue();
const textHeadingStyle = {
@@ -149,28 +148,22 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
const getProgressDescription = (): string => {
const processedCount = portalDataTransferJob?.properties?.processedCount;
const totalCount = portalDataTransferJob?.properties?.totalCount;
const processedCountString =
totalCount > 0
? t(Keys.controls.settings.partitionKeyEditor.documentsProcessed, {
processedCount: String(processedCount),
totalCount: String(totalCount),
})
: "";
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : "";
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
};
const startPartitionkeyChangeWorkflow = () => {
useSidePanel.getState().openSidePanel(
t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
partitionKeyName: t(Keys.controls.settings.partitionKey.partitionKey).toLowerCase(),
}),
<ChangePartitionKeyPane
sourceDatabase={database}
sourceCollection={collection}
explorer={explorer}
onClose={refreshDataTransferOperations}
/>,
);
useSidePanel
.getState()
.openSidePanel(
"Change partition key",
<ChangePartitionKeyPane
sourceDatabase={database}
sourceCollection={collection}
explorer={explorer}
onClose={refreshDataTransferOperations}
/>,
);
};
const getPercentageComplete = () => {
@@ -188,28 +181,16 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
return (
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
<Stack tokens={{ childrenGap: 10 }}>
{!isReadOnly && (
<Text styles={textHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
partitionKeyName: partitionKeyName.toLowerCase(),
})}
</Text>
)}
{!isReadOnly && <Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>}
<Stack horizontal tokens={{ childrenGap: 20 }}>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={textSubHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.currentPartitionKey, {
partitionKeyName: partitionKeyName.toLowerCase(),
})}
</Text>
<Text styles={textSubHeadingStyle}>{t(Keys.controls.settings.partitionKeyEditor.partitioning)}</Text>
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
<Text styles={textSubHeadingStyle}>Partitioning</Text>
</Stack>
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
<Text styles={textSubHeadingStyle1}>
{isHierarchicalPartitionedContainer()
? t(Keys.controls.settings.partitionKeyEditor.hierarchical)
: t(Keys.controls.settings.partitionKeyEditor.nonHierarchical)}
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
</Text>
</Stack>
</Stack>
@@ -223,33 +204,33 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={darkThemeMessageBarStyles}
>
{t(Keys.controls.settings.partitionKeyEditor.safeguardWarning)}
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.
<Link
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank"
underline
style={{ color: "var(--colorBrandForeground1)" }}
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</MessageBar>
<Text styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
{t(Keys.controls.settings.partitionKeyEditor.changeDescription)}
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>
{configContext.platform !== Platform.Emulator && (
<PrimaryButton
data-test="change-partition-key-button"
styles={{ root: { width: "fit-content" } }}
text={t(Keys.controls.settings.partitionKeyEditor.changeButton)}
text="Change"
onClick={startPartitionkeyChangeWorkflow}
disabled={isCurrentJobInProgress(portalDataTransferJob)}
/>
)}
{portalDataTransferJob && (
<Stack>
<Text styles={textHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.changeJob, { partitionKeyName })}
</Text>
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
<Stack
horizontal
tokens={{ childrenGap: 20 }}
@@ -270,10 +251,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
}}
></ProgressIndicator>
{isCurrentJobInProgress(portalDataTransferJob) && (
<DefaultButton
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)}
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)}
/>
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
)}
</Stack>
</Stack>

View File

@@ -1,5 +1,4 @@
import { Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
import { Keys, t } from "Localization";
import * as React from "react";
import * as Constants from "../../../../Common/Constants";
import { Platform, configContext } from "../../../../ConfigContext";
@@ -93,10 +92,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
}
const minThroughput: string = this.getMinRUs().toLocaleString();
const maxThroughput: string = !this.props.isFixedContainer
? t(Keys.controls.settings.scale.unlimited)
: this.getMaxRUs().toLocaleString();
return t(Keys.controls.settings.scale.throughputRangeLabel, { min: minThroughput, max: maxThroughput });
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
};
public canThroughputExceedMaximumValue = (): boolean => {
@@ -159,12 +156,14 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
const freeTierLimits = SharedConstants.FreeTierLimits;
return (
<Text>
{t(Keys.controls.settings.scale.freeTierInfo, { ru: freeTierLimits.RU, storage: freeTierLimits.Storage })}
With free tier, you will get the first {freeTierLimits.RU} RU/s and {freeTierLimits.Storage} GB of storage in
this account for free. To keep your account free, keep the total RU/s across all resources in the account to{" "}
{freeTierLimits.RU} RU/s.
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank"
>
{t(Keys.controls.settings.scale.freeTierLearnMore)}
Learn more.
</Link>
</Text>
);
@@ -189,9 +188,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
{/* TODO: Replace link with call to the Azure Support blade */}
{this.isAutoScaleEnabled() && (
<Stack {...titleAndInputStackProps}>
<Text>{t(Keys.controls.settings.scale.throughputRuS)}</Text>
<Text>Throughput (RU/s)</Text>
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
<Text>{t(Keys.controls.settings.scale.autoScaleCustomSettings)}</Text>
<Text>
Your account has custom settings that prevents setting throughput at the container level. Please work with
your Cosmos DB engineering team point of contact to make changes.
</Text>
</Stack>
)}
</Stack>

View File

@@ -12,7 +12,6 @@ import {
} from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { userContext } from "../../../../UserContext";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
@@ -86,12 +85,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
constructor(props: SubSettingsComponentProps) {
super(props);
this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyName =
userContext.apiType === "Mongo"
? t(Keys.controls.settings.partitionKey.shardKey)
: t(Keys.controls.settings.partitionKey.partitionKey);
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
this.partitionKeyValue = this.getPartitionKeyValue();
this.uniqueKeyName = t(Keys.controls.settings.subSettings.uniqueKeys);
this.uniqueKeyName = "Unique keys";
this.uniqueKeyValue = this.getUniqueKeyValue();
}
@@ -147,13 +143,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
};
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), ariaLabel: "ttl-off-option" },
{
key: TtlType.OnNoDefault,
text: t(Keys.controls.settings.subSettings.ttlOnNoDefault),
ariaLabel: "ttl-on-no-default-option",
},
{ key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn), ariaLabel: "ttl-on-option" },
{ key: TtlType.Off, text: "Off", ariaLabel: "ttl-off-option" },
{ key: TtlType.OnNoDefault, text: "On (no default)", ariaLabel: "ttl-on-no-default-option" },
{ key: TtlType.On, text: "On", ariaLabel: "ttl-on-option" },
];
public getTtlValue = (value: string): TtlType => {
@@ -224,13 +216,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
}}
>
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.subSettings.mongoTtlMessage)}{" "}
To enable time-to-live (TTL) for your collection/documents,{" "}
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live"
target="_blank"
style={{ color: "var(--colorBrandForeground1)" }}
>
{t(Keys.controls.settings.subSettings.mongoTtlLinkText)}
create a TTL index
</Link>
.
</Text>
@@ -239,7 +231,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="timeToLive"
label={t(Keys.controls.settings.subSettings.timeToLive)}
label="Time to Live"
selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions}
onChange={this.onTtlChange}
@@ -263,8 +255,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
max={Int32.Max}
value={this.props.displayedTtlSeconds}
onChange={this.onTimeToLiveSecondsChange}
suffix={t(Keys.controls.settings.subSettings.seconds)}
ariaLabel={t(Keys.controls.settings.subSettings.timeToLiveInSeconds)}
suffix="second(s)"
ariaLabel={`Time to live in seconds`}
data-test="ttl-input"
/>
)}
@@ -272,16 +264,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), disabled: true },
{ key: TtlType.OnNoDefault, text: t(Keys.controls.settings.subSettings.ttlOnNoDefault) },
{ key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
{ key: TtlType.Off, text: "Off", disabled: true },
{ key: TtlType.OnNoDefault, text: "On (no default)" },
{ key: TtlType.On, text: "On" },
];
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="analyticalStorageTimeToLive"
label={t(Keys.controls.settings.subSettings.analyticalStorageTtl)}
label="Analytical Storage Time to Live"
selectedKey={this.props.analyticalStorageTtlSelection}
options={this.analyticalTtlChoiceGroupOptions}
onChange={this.onAnalyticalStorageTtlSelectionChange}
@@ -302,7 +294,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
min={1}
max={Int32.Max}
value={this.props.analyticalStorageTtlSeconds?.toString()}
suffix={t(Keys.controls.settings.subSettings.seconds)}
suffix="second(s)"
onChange={this.onAnalyticalStorageTtlSecondsChange}
/>
)}
@@ -310,22 +302,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{
key: GeospatialConfigType.Geography,
text: t(Keys.controls.settings.subSettings.geography),
ariaLabel: "geography-option",
},
{
key: GeospatialConfigType.Geometry,
text: t(Keys.controls.settings.subSettings.geometry),
ariaLabel: "geometry-option",
},
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
];
private getGeoSpatialComponent = (): JSX.Element => (
<ChoiceGroup
id="geoSpatialConfig"
label={t(Keys.controls.settings.subSettings.geospatialConfiguration)}
label="Geospatial Configuration"
selectedKey={this.props.geospatialConfigType}
options={this.geoSpatialConfigTypeChoiceGroupOptions}
onChange={this.onGeoSpatialConfigTypeChange}
@@ -334,8 +318,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: ChangeFeedPolicyState.Off, text: t(Keys.controls.settings.subSettings.ttlOff) },
{ key: ChangeFeedPolicyState.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
{ key: ChangeFeedPolicyState.Off, text: "Off" },
{ key: ChangeFeedPolicyState.On, text: "On" },
];
private getChangeFeedComponent = (): JSX.Element => {
@@ -344,10 +328,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
return (
<Stack>
<Label id={labelId}>
<ToolTipLabelComponent
label={t(Keys.controls.settings.changeFeed.label)}
toolTipElement={changeFeedPolicyToolTip}
/>
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} />
</Label>
<ChoiceGroup
id="changeFeedPolicy"
@@ -373,10 +354,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
<Stack {...titleAndInputStackProps}>
{this.getPartitionKeyVisible() && (
<TooltipHost
content={t(Keys.controls.settings.subSettings.partitionKeyTooltipTemplate, {
partitionKeyName: this.partitionKeyName.toLowerCase(),
partitionKeyValue: this.partitionKeyValue,
})}
content={`This ${this.partitionKeyName.toLowerCase()} is used to distribute data across multiple partitions for scalability. The value "${
this.partitionKeyValue
}" determines how documents are partitioned.`}
styles={{
root: {
display: "block",
@@ -393,20 +373,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
)}
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
<Text className={classNames.hintText}>
{t(Keys.controls.settings.subSettings.largePartitionKeyEnabled, {
partitionKeyName: this.partitionKeyName.toLowerCase(),
})}
</Text>
<Text className={classNames.hintText}>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
)}
{userContext.apiType === "SQL" &&
(this.isHierarchicalPartitionedContainer() ? (
<Text className={classNames.hintText}>{t(Keys.controls.settings.subSettings.hierarchicalPartitioned)}</Text>
<Text className={classNames.hintText}>Hierarchically partitioned container.</Text>
) : (
<Text className={classNames.hintText}>
{t(Keys.controls.settings.subSettings.nonHierarchicalPartitioned)}
</Text>
<Text className={classNames.hintText}>Non-hierarchically partitioned container.</Text>
))}
</Stack>
);

View File

@@ -1,6 +1,5 @@
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
import { ThroughputBucket } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React, { FC, useEffect, useState } from "react";
import { isDirty } from "../../SettingsUtils";
@@ -66,9 +65,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
return (
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
{t(Keys.controls.settings.throughputBuckets.label)}
</Label>
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>Throughput Buckets</Label>
<Stack>
{throughputBuckets?.map((bucket) => (
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
@@ -79,9 +76,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
value={bucket.maxThroughputPercentage}
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
showValue={false}
label={`${t(Keys.controls.settings.throughputBuckets.bucketLabel, { id: String(bucket.id) })}${
bucket.id === 1 ? t(Keys.controls.settings.throughputBuckets.dataExplorerQueryBucket) : ""
}`}
label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
styles={{
root: { flex: 2, maxWidth: 400 },
titleLabel: {
@@ -104,8 +99,8 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
disabled={bucket.maxThroughputPercentage === 100}
/>
<Toggle
onText={t(Keys.controls.settings.throughputBuckets.active)}
offText={t(Keys.controls.settings.throughputBuckets.inactive)}
onText="Active"
offText="Inactive"
checked={bucket.maxThroughputPercentage !== 100}
onChange={(event, checked) => onToggle(bucket.id, checked)}
styles={{

View File

@@ -16,7 +16,6 @@ import {
Text,
TextField,
} from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../../Shared/Constants";
@@ -98,8 +97,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private throughputInputMaxValue: number;
private autoPilotInputMaxValue: number;
private options: IChoiceGroupOption[] = [
{ key: "true", text: t(Keys.controls.settings.throughputInput.autoscale) },
{ key: "false", text: t(Keys.controls.settings.throughputInput.manual) },
{ key: "true", text: "Autoscale" },
{ key: "false", text: "Manual" },
];
// Style constants for theme-aware colors and layout
@@ -245,7 +244,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return (
<div>
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
Updated cost per month
</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text
@@ -254,8 +253,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}{" "}
{t(Keys.controls.settings.throughputInput.min)}
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
</Text>
<Text
style={{
@@ -263,8 +261,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}{" "}
{t(Keys.controls.settings.throughputInput.max)}
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
</Text>
</Stack>
</div>
@@ -277,7 +274,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
Current cost per month
</Text>
<Stack horizontal style={{ marginTop: 5, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
<Text
@@ -286,8 +283,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}{" "}
{t(Keys.controls.settings.throughputInput.min)}
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
</Text>
<Text
style={{
@@ -295,8 +291,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}{" "}
{t(Keys.controls.settings.throughputInput.max)}
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
</Text>
</Stack>
</Stack>
@@ -331,20 +326,17 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return (
<div>
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
Updated cost per month
</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text style={this.settingsAndScaleStyle.root}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
{t(Keys.controls.settings.costEstimate.perHour)}
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr
</Text>
<Text style={this.settingsAndScaleStyle.root}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
{t(Keys.controls.settings.costEstimate.perDay)}
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day
</Text>
<Text style={this.settingsAndScaleStyle.root}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
{t(Keys.controls.settings.costEstimate.perMonth)}
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
</Text>
</Stack>
</div>
@@ -357,20 +349,17 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
Current cost per month
</Text>
<Stack horizontal style={{ marginTop: 5 }}>
<Text style={this.settingsAndScaleStyle.root}>
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
{t(Keys.controls.settings.costEstimate.perHour)}
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr
</Text>
<Text style={this.settingsAndScaleStyle.root}>
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
{t(Keys.controls.settings.costEstimate.perDay)}
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day
</Text>
<Text style={this.settingsAndScaleStyle.root}>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
{t(Keys.controls.settings.costEstimate.perMonth)}
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo
</Text>
</Stack>
</Stack>
@@ -455,14 +444,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
this.setState({ spendAckChecked: checked });
private getStorageCapacityTitle = (): JSX.Element => {
const capacity: string = this.props.isFixed
? t(Keys.controls.settings.throughputInput.fixed)
: t(Keys.controls.settings.throughputInput.unlimited);
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.throughputInput.storageCapacity)}
</Label>
<Label style={{ color: "var(--colorNeutralForeground1)" }}>Storage capacity</Label>
<Text style={{ color: "var(--colorNeutralForeground1)" }}>{capacity}</Text>
</Stack>
);
@@ -570,14 +555,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
/>
<Stack horizontal>
<Stack.Item style={{ width: "34%", paddingRight: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>
{t(Keys.controls.settings.throughputInput.instant)}
</Separator>
<Separator styles={this.thoughputRangeSeparatorStyles}>Instant</Separator>
</Stack.Item>
<Stack.Item style={{ width: "66%", paddingLeft: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>
{t(Keys.controls.settings.throughputInput.fourToSixHrs)}
</Separator>
<Separator styles={this.thoughputRangeSeparatorStyles}>4-6 hrs</Separator>
</Stack.Item>
</Stack>
</Stack>
@@ -657,7 +638,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
{t(Keys.controls.settings.throughputInput.minimumRuS)}
Minimum RU/s
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
</Stack>
@@ -691,7 +672,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: "var(--colorNeutralForeground1)",
}}
>
{t(Keys.controls.settings.throughputInput.x10Equals)}
x 10 =
</Text>
{/* Column 3: Maximum RU/s */}
@@ -701,7 +682,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
{t(Keys.controls.settings.throughputInput.maximumRuS)}
Maximum RU/s
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
</Stack>
@@ -742,9 +723,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
const errorMessage: string =
sanitizedValue % 1000
? t(Keys.controls.settings.throughput.throughputIncrementError)
: this.props.throughputError;
sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError;
return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
}}
validateOnLoad={false}
@@ -790,9 +769,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
)}
{this.props.isAutoPilotSelected ? (
<Text style={{ marginTop: "40px", color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.throughputInput.autoscaleDescription, {
resourceType: this.props.collectionName ? "container" : "database",
})}{" "}
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
{this.props.maxAutoPilotThroughput} RU/s
@@ -807,19 +784,16 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
styles={this.darkThemeMessageBarStyles}
style={{ marginTop: "40px" }}
>
{t(Keys.controls.settings.throughputInput.freeTierWarning, {
ru: String(SharedConstants.FreeTierLimits.RU),
})}
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
</MessageBar>
)}
</>
)}
{!this.overrideWithProvisionedThroughputSettings() && (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.throughputInput.capacityCalculator)}
Estimate your required RU/s with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{t(Keys.controls.settings.throughputInput.capacityCalculatorLink)}{" "}
<FontIcon iconName="NavigateExternalInline" />
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
</Link>
</Text>
)}
@@ -832,7 +806,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onSpendAckChecked}
/>
)}
{this.props.isFixed && <p>{t(Keys.controls.settings.throughputInput.fixedStorageNote)}</p>}
{this.props.isFixed && (
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
)}
{this.props.collectionName && (
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
)}

View File

@@ -552,7 +552,9 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
@@ -570,7 +572,9 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
Based on usage, your container throughput will scale from
Based on usage, your
container
throughput will scale from
<b>
400
@@ -679,8 +683,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
35.04
min
min
</Text>
<Text
style={
@@ -693,8 +696,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
350.40
max
max
</Text>
</Stack>
</div>
@@ -728,8 +730,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
35.04
min
min
</Text>
<Text
style={
@@ -742,8 +743,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
350.40
max
max
</Text>
</Stack>
</Stack>
@@ -1151,7 +1151,9 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
@@ -1726,7 +1728,9 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"

View File

@@ -27,8 +27,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
iconName="NavigateExternalInline"
/>
</StyledLinkBase>
 
about how to define computed properties and how to use them.
  about how to define computed properties and how to use them.
</Text>
<div
className="settingsV2Editor"

View File

@@ -33,7 +33,8 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
}
}
>
Change partition key
Change
partition key
</Text>
<Stack
horizontal={true}
@@ -60,7 +61,8 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
}
}
>
Current partition key
Current
partition key
</Text>
<Text
styles={
@@ -221,7 +223,8 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
}
}
>
Current partition key
Current
partition key
</Text>
<Text
styles={

View File

@@ -410,7 +410,9 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
<Text
className="hintText-115"
>
Large partition key has been enabled.
Large
partition key
has been enabled.
</Text>
<Text
className="hintText-115"
@@ -982,7 +984,9 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
<Text
className="hintText-115"
>
Large partition key has been enabled.
Large
partition key
has been enabled.
</Text>
<Text
className="hintText-115"
@@ -1518,7 +1522,9 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
<Text
className="hintText-115"
>
Large partition key has been enabled.
Large
partition key
has been enabled.
</Text>
<Text
className="hintText-115"
@@ -2151,7 +2157,9 @@ exports[`SubSettingsComponent renders 1`] = `
<Text
className="hintText-115"
>
Large partition key has been enabled.
Large
partition key
has been enabled.
</Text>
<Text
className="hintText-115"
@@ -2721,7 +2729,9 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
<Text
className="hintText-115"
>
Large partition key has been enabled.
Large
partition key
has been enabled.
</Text>
<Text
className="hintText-115"

View File

@@ -1,7 +1,6 @@
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
import { userContext } from "../../../UserContext";
import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils";
@@ -176,27 +175,25 @@ const getStringValue = (value: isDirtyTypes, type: string): string => {
export const getTabTitle = (tab: SettingsV2TabTypes): string => {
switch (tab) {
case SettingsV2TabTypes.ScaleTab:
return t(Keys.controls.settings.tabTitles.scale);
return "Scale";
case SettingsV2TabTypes.ConflictResolutionTab:
return t(Keys.controls.settings.tabTitles.conflictResolution);
return "Conflict Resolution";
case SettingsV2TabTypes.SubSettingsTab:
return t(Keys.controls.settings.tabTitles.settings);
return "Settings";
case SettingsV2TabTypes.IndexingPolicyTab:
return t(Keys.controls.settings.tabTitles.indexingPolicy);
return "Indexing Policy";
case SettingsV2TabTypes.PartitionKeyTab:
return isFabricNative()
? t(Keys.controls.settings.tabTitles.partitionKeys)
: t(Keys.controls.settings.tabTitles.partitionKeysPreview);
return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)";
case SettingsV2TabTypes.ComputedPropertiesTab:
return t(Keys.controls.settings.tabTitles.computedProperties);
return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab:
return t(Keys.controls.settings.tabTitles.containerPolicies);
return "Container Policies";
case SettingsV2TabTypes.ThroughputBucketsTab:
return t(Keys.controls.settings.tabTitles.throughputBuckets);
return "Throughput Buckets";
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
return t(Keys.controls.settings.tabTitles.globalSecondaryIndexPreview);
return "Global Secondary Index (Preview)";
case SettingsV2TabTypes.DataMaskingTab:
return t(Keys.controls.settings.tabTitles.maskingPolicyPreview);
return "Masking Policy (preview)";
default:
throw new Error(`Unknown tab ${tab}`);
}
@@ -206,19 +203,19 @@ export const getMongoNotification = (description: string, type: MongoIndexTypes)
if (description && !type) {
return {
type: MongoNotificationType.Warning,
message: t(Keys.controls.settings.mongoNotifications.selectTypeWarning),
message: "Please select a type for each index.",
};
}
if (type && (!description || description.trim().length === 0)) {
return {
type: MongoNotificationType.Error,
message: t(Keys.controls.settings.mongoNotifications.enterFieldNameError),
message: "Please enter a field name.",
};
} else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) {
return {
type: MongoNotificationType.Error,
message: t(Keys.controls.settings.mongoNotifications.wildcardPathError) + MongoWildcardPlaceHolder,
message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder,
};
}
@@ -252,29 +249,28 @@ export const isIndexTransforming = (indexTransformationProgress: number): boolea
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
const partitionKeyName =
apiType === "Mongo"
? t(Keys.controls.settings.partitionKey.shardKey)
: t(Keys.controls.settings.partitionKey.partitionKey);
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
};
export const getPartitionKeyTooltipText = (apiType: string): string => {
if (apiType === "Mongo") {
return t(Keys.controls.settings.partitionKey.shardKeyTooltip);
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${getPartitionKeyName(apiType, true)} ${t(
Keys.controls.settings.partitionKey.partitionKeyTooltip,
)}`;
let tooltipText = `The ${getPartitionKeyName(
apiType,
true,
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
if (apiType === "SQL") {
tooltipText += t(Keys.controls.settings.partitionKey.sqlPartitionKeyTooltipSuffix);
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
};
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
return t(Keys.controls.settings.partitionKey.partitionKeySubtext);
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
return subtext;
}
return "";
};
@@ -282,18 +278,18 @@ export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: st
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
switch (apiType) {
case "Mongo":
return t(Keys.controls.settings.partitionKey.mongoPlaceholder);
return "e.g., categoryId";
case "Gremlin":
return t(Keys.controls.settings.partitionKey.gremlinPlaceholder);
return "e.g., /address";
case "SQL":
return `${
index === undefined
? t(Keys.controls.settings.partitionKey.sqlFirstPartitionKey)
? "Required - first partition key e.g., /TenantId"
: index === 0
? t(Keys.controls.settings.partitionKey.sqlSecondPartitionKey)
: t(Keys.controls.settings.partitionKey.sqlThirdPartitionKey)
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return t(Keys.controls.settings.partitionKey.defaultPlaceholder);
return "e.g., /address/zipCode";
}
};

View File

@@ -110,7 +110,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -232,7 +231,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -455,7 +453,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -527,7 +524,6 @@ exports[`SettingsComponent renders 1`] = `
explorer={
Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -696,7 +692,6 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -768,7 +763,6 @@ exports[`SettingsComponent renders 1`] = `
explorer={
Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],

View File

@@ -127,13 +127,9 @@ exports[`SettingsUtils functions render 1`] = `
>
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
Database:
sampleDb
,
Container:
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
@@ -313,7 +309,7 @@ exports[`SettingsUtils functions render 1`] = `
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}
>

View File

@@ -113,7 +113,7 @@ export class ContainerSampleGenerator {
? await createMongoDocument(collection.databaseId, collection, shardKey, doc)
: await createDocument(collection, doc);
} catch (error) {
NotificationConsoleUtils.logConsoleError(error instanceof Error ? error.message : String(error));
NotificationConsoleUtils.logConsoleError(error);
}
}),
);

View File

@@ -1,168 +0,0 @@
jest.mock("Utils/arm/generatedClients/cosmos/databaseAccounts");
jest.mock("Utils/NotificationConsoleUtils", () => ({
logConsoleProgress: jest.fn(() => jest.fn()), // returns a clearMessage fn
logConsoleInfo: jest.fn(),
logConsoleError: jest.fn(),
}));
jest.mock("Shared/Telemetry/TelemetryProcessor");
import { DatabaseAccount } from "../Contracts/DataModels";
import { updateUserContext, userContext } from "../UserContext";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import Explorer from "./Explorer";
const mockUpdate = update as jest.MockedFunction<typeof update>;
// Capture `useDialog.getState().openDialog` calls
const mockOpenDialog = jest.fn();
const mockCloseDialog = jest.fn();
jest.mock("./Controls/Dialog", () => ({
useDialog: {
getState: jest.fn(() => ({
openDialog: mockOpenDialog,
closeDialog: mockCloseDialog,
})),
},
}));
// Silence useNotebook subscription calls
jest.mock("./Notebook/useNotebook", () => ({
useNotebook: {
subscribe: jest.fn(),
getState: jest.fn().mockReturnValue(
new Proxy(
{},
{
get: () => jest.fn().mockResolvedValue(undefined),
},
),
),
},
}));
describe("Explorer.openEnableSynapseLinkDialog", () => {
let explorer: Explorer;
const baseAccount: DatabaseAccount = {
id: "/subscriptions/ctx-sub/resourceGroups/ctx-rg/providers/Microsoft.DocumentDB/databaseAccounts/ctx-account",
name: "ctx-account",
location: "East US",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
tags: {},
properties: {
documentEndpoint: "https://ctx-account.documents.azure.com:443/",
capabilities: [],
enableMultipleWriteLocations: false,
},
};
beforeAll(() => {
updateUserContext({
databaseAccount: baseAccount,
subscriptionId: "ctx-sub",
resourceGroup: "ctx-rg",
});
});
beforeEach(() => {
jest.clearAllMocks();
mockUpdate.mockResolvedValue(undefined);
explorer = new Explorer();
});
describe("without targetAccountOverride", () => {
it("should open a dialog when called without override", () => {
explorer.openEnableSynapseLinkDialog();
expect(mockOpenDialog).toHaveBeenCalledTimes(1);
});
it("should use userContext values in the update call on primary button click", async () => {
explorer.openEnableSynapseLinkDialog();
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(mockUpdate).toHaveBeenCalledWith(
"ctx-sub",
"ctx-rg",
"ctx-account",
expect.objectContaining({
properties: { enableAnalyticalStorage: true },
}),
);
});
it("should update userContext.databaseAccount.properties when no override is provided", async () => {
explorer.openEnableSynapseLinkDialog();
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(userContext.databaseAccount.properties.enableAnalyticalStorage).toBe(true);
});
});
describe("with targetAccountOverride", () => {
const override = {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
};
it("should open a dialog when called with override", () => {
explorer.openEnableSynapseLinkDialog(override);
expect(mockOpenDialog).toHaveBeenCalledTimes(1);
});
it("should use override values in the update call on primary button click", async () => {
explorer.openEnableSynapseLinkDialog(override);
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(mockUpdate).toHaveBeenCalledWith(
"override-sub",
"override-rg",
"override-account",
expect.objectContaining({
properties: { enableAnalyticalStorage: true },
}),
);
});
it("should NOT update userContext.databaseAccount.properties when override is provided", async () => {
// Reset the property first
userContext.databaseAccount.properties.enableAnalyticalStorage = false;
explorer.openEnableSynapseLinkDialog(override);
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(userContext.databaseAccount.properties.enableAnalyticalStorage).toBe(false);
});
it("should use override values — NOT userContext — even when userContext has different values", async () => {
explorer.openEnableSynapseLinkDialog(override);
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
// update should NOT be called with ctx-sub / ctx-rg / ctx-account
expect(mockUpdate).not.toHaveBeenCalledWith("ctx-sub", expect.anything(), expect.anything(), expect.anything());
});
});
describe("secondary button click", () => {
it("should close the dialog on secondary button click", () => {
explorer.openEnableSynapseLinkDialog();
const dialogProps = mockOpenDialog.mock.calls[0][0];
dialogProps.onSecondaryButtonClick();
expect(mockCloseDialog).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -107,12 +107,6 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5;
public phoenixClient: PhoenixClient;
/**
* Resolves when the initial refreshAllDatabases (including collection loading) completes.
* Await this instead of calling refreshAllDatabases again to avoid duplicate concurrent loads.
*/
public databasesRefreshed: Promise<void> = Promise.resolve();
constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
@@ -227,11 +221,7 @@ export default class Explorer {
this.refreshNotebookList();
}
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;
public openEnableSynapseLinkDialog(): void {
const addSynapseLinkDialogProps: DialogProps = {
linkProps: {
linkText: "Learn more",
@@ -253,7 +243,7 @@ export default class Explorer {
useDialog.getState().closeDialog();
try {
await update(subscriptionId, resourceGroup, accountName, {
await update(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, {
properties: {
enableAnalyticalStorage: true,
},
@@ -262,9 +252,7 @@ export default class Explorer {
clearInProgressMessage();
logConsoleInfo("Enabled Azure Synapse Link for this account");
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, {}, startTime);
if (!targetAccountOverride) {
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
}
userContext.databaseAccount.properties.enableAnalyticalStorage = true;
} catch (error) {
clearInProgressMessage();
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
@@ -1209,11 +1197,9 @@ export default class Explorer {
}
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
this.databasesRefreshed =
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
await this.databasesRefreshed; // await: we rely on the databases to be loaded before restoring the tabs further in the flow
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
}
if (!isFabricNative()) {

View File

@@ -167,7 +167,7 @@ export function createContextCommandBarButtons(
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [
ThemeToggleButton(configContext.platform === Platform.Portal),
ThemeToggleButton(),
{
iconSrc: SettingsIcon,
iconAlt: "Settings",

View File

@@ -5,7 +5,6 @@ import {
IconType,
IDropdownOption,
IDropdownStyles,
TooltipHost,
} from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts";
@@ -155,21 +154,6 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
id: btn.id,
};
if (btn.tooltipContent) {
result.title = undefined;
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
const { defaultRender: DefaultRender, ...rest } = props;
return React.createElement(
TooltipHost,
{
content: btn.tooltipContent as JSX.Element,
calloutProps: { gapSpace: 0 },
},
React.createElement(DefaultRender, rest),
);
};
}
if (isSplit) {
// It's a split button
result.split = true;

View File

@@ -1,13 +1,10 @@
import { Link, Text } from "@fluentui/react";
import * as React from "react";
import MoonIcon from "../../../../images/MoonIcon.svg";
import SunIcon from "../../../../images/SunIcon.svg";
import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
const PORTAL_SETTINGS_URL = "https://learn.microsoft.com/azure/azure-portal/set-preferences";
export const ThemeToggleButton = (isPortal?: boolean): CommandButtonComponentProps => {
export const ThemeToggleButton = (): CommandButtonComponentProps => {
const [darkMode, setDarkMode] = React.useState(useThemeStore.getState().isDarkMode);
React.useEffect(() => {
@@ -19,34 +16,6 @@ export const ThemeToggleButton = (isPortal?: boolean): CommandButtonComponentPro
const tooltipText = darkMode ? "Switch to Light Theme" : "Switch to Dark Theme";
if (isPortal) {
return {
iconSrc: darkMode ? SunIcon : MoonIcon,
iconAlt: "Theme Toggle",
onCommandClick: undefined,
commandButtonLabel: undefined,
ariaLabel: "Dark Mode is managed in Azure Portal Settings",
tooltipText: undefined,
tooltipContent: React.createElement(
"div",
{ style: { padding: "4px 0" } },
React.createElement(Text, { block: true, variant: "small" }, "Dark Mode is managed in Azure Portal Settings"),
React.createElement(
Link,
{
href: PORTAL_SETTINGS_URL,
target: "_blank",
rel: "noopener noreferrer",
style: { display: "inline-block", marginTop: "4px", fontSize: "12px" },
},
"Open settings",
),
),
hasPopup: false,
disabled: true,
};
}
return {
iconSrc: darkMode ? SunIcon : MoonIcon,
iconAlt: "Theme Toggle",

View File

@@ -329,10 +329,7 @@ export class NotificationConsoleComponent extends React.Component<
}
private static extractHeaderStatus(consoleData: ConsoleData) {
if (!consoleData?.message || typeof consoleData.message !== "string") {
return undefined;
}
return consoleData.message.split(":\n")[0];
return consoleData?.message.split(":\n")[0];
}
private onConsoleWasExpanded = (): void => {

View File

@@ -12,56 +12,4 @@ describe("AddCollectionPanel", () => {
const wrapper = shallow(<AddCollectionPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("targetAccountOverride prop", () => {
it("should render with targetAccountOverride prop set", () => {
const override = {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
};
const wrapper = shallow(<AddCollectionPanel {...props} targetAccountOverride={override} />);
expect(wrapper).toBeDefined();
});
it("should pass targetAccountOverride to openEnableSynapseLinkDialog button click", () => {
const mockOpenEnableSynapseLinkDialog = jest.fn();
const explorerWithMock = { ...props.explorer, openEnableSynapseLinkDialog: mockOpenEnableSynapseLinkDialog };
const override = {
subscriptionId: "override-sub",
resourceGroup: "override-rg",
accountName: "override-account",
};
const wrapper = shallow(
<AddCollectionPanel explorer={explorerWithMock as unknown as Explorer} targetAccountOverride={override} />,
);
// isSynapseLinkEnabled section requires specific conditions; verify the component exists
expect(wrapper).toBeDefined();
});
});
describe("externalDatabaseOptions prop", () => {
it("should accept externalDatabaseOptions without error", () => {
const externalOptions = [
{ key: "db1", text: "Database One" },
{ key: "db2", text: "Database Two" },
];
const wrapper = shallow(<AddCollectionPanel {...props} externalDatabaseOptions={externalOptions} />);
expect(wrapper).toBeDefined();
});
});
describe("isCopyJobFlow prop", () => {
it("should render with isCopyJobFlow=true", () => {
const wrapper = shallow(<AddCollectionPanel {...props} isCopyJobFlow={true} />);
expect(wrapper).toBeDefined();
});
it("should render with isCopyJobFlow=false (default behaviour)", () => {
const wrapper = shallow(<AddCollectionPanel {...props} isCopyJobFlow={false} />);
expect(wrapper).toBeDefined();
});
});
});

View File

@@ -21,7 +21,6 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { AccountOverride } from "Contracts/DataModels";
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import {
@@ -43,7 +42,6 @@ import {
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import { Keys, t } from "Localization";
import { DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT, isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { CollectionCreation } from "Shared/Constants";
@@ -69,8 +67,6 @@ export interface AddCollectionPanelProps {
isQuickstart?: boolean;
isCopyJobFlow?: boolean;
onSubmitSuccess?: (collectionData: { databaseId: string; collectionId: string }) => void;
targetAccountOverride?: AccountOverride;
externalDatabaseOptions?: IDropdownOption[];
}
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
@@ -181,31 +177,31 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
linkText={t(Keys.common.learnMore)}
linkText="Learn more"
/>
)}
{this.state.teachingBubbleStep === 1 && (
<TeachingBubble
headline={t(Keys.panes.addCollection.teachingBubble.step1Headline)}
headline="Create sample database"
target={"#newDatabaseId"}
calloutProps={{ gapSpace: 16 }}
primaryButtonProps={{ text: t(Keys.common.next), onClick: () => this.setState({ teachingBubbleStep: 2 }) }}
secondaryButtonProps={{
text: t(Keys.common.cancel),
onClick: () => this.setState({ teachingBubbleStep: 0 }),
}}
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 2 }) }}
secondaryButtonProps={{ text: "Cancel", onClick: () => this.setState({ teachingBubbleStep: 0 }) }}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent={t(Keys.panes.addCollection.teachingBubble.stepOfTotal, { current: "1", total: "4" })}
footerContent="Step 1 of 4"
>
<Stack>
<Text style={{ color: "white" }}>{t(Keys.panes.addCollection.teachingBubble.step1Body)}</Text>
<Text style={{ color: "white" }}>
Database is the parent of a container. You can create a new database or use an existing one. In this
tutorial we are creating a new database named SampleDB.
</Text>
<Link
style={{ color: "white", fontWeight: 600 }}
target="_blank"
href="https://aka.ms/TeachingbubbleResources"
>
{t(Keys.panes.addCollection.teachingBubble.step1LearnMore)}
Learn more about resources.
</Link>
</Stack>
</TeachingBubble>
@@ -213,21 +209,21 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.state.teachingBubbleStep === 2 && (
<TeachingBubble
headline={t(Keys.panes.addCollection.teachingBubble.step2Headline)}
headline="Setting throughput"
target={"#autoscaleRUValueField"}
calloutProps={{ gapSpace: 16 }}
primaryButtonProps={{ text: t(Keys.common.next), onClick: () => this.setState({ teachingBubbleStep: 3 }) }}
secondaryButtonProps={{
text: t(Keys.common.previous),
onClick: () => this.setState({ teachingBubbleStep: 1 }),
}}
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 3 }) }}
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 1 }) }}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent={t(Keys.panes.addCollection.teachingBubble.stepOfTotal, { current: "2", total: "4" })}
footerContent="Step 2 of 4"
>
<Stack>
<Text style={{ color: "white" }}>{t(Keys.panes.addCollection.teachingBubble.step2Body)}</Text>
<Text style={{ color: "white" }}>
Cosmos DB recommends sharing throughput across database. Autoscale will give you a flexible amount of
throughput based on the max RU/s set (Request Units).
</Text>
<Link style={{ color: "white", fontWeight: 600 }} target="_blank" href="https://aka.ms/teachingbubbleRU">
{t(Keys.panes.addCollection.teachingBubble.step2LearnMore)}
Learn more about RU/s.
</Link>
</Stack>
</TeachingBubble>
@@ -235,41 +231,36 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.state.teachingBubbleStep === 3 && (
<TeachingBubble
headline={t(Keys.panes.addCollection.teachingBubble.step3Headline)}
headline="Naming container"
target={"#collectionId"}
calloutProps={{ gapSpace: 16 }}
primaryButtonProps={{ text: t(Keys.common.next), onClick: () => this.setState({ teachingBubbleStep: 4 }) }}
secondaryButtonProps={{
text: t(Keys.common.previous),
onClick: () => this.setState({ teachingBubbleStep: 2 }),
}}
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 4 }) }}
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 2 }) }}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent={t(Keys.panes.addCollection.teachingBubble.stepOfTotal, { current: "3", total: "4" })}
footerContent="Step 3 of 4"
>
{t(Keys.panes.addCollection.teachingBubble.step3Body)}
Name your container
</TeachingBubble>
)}
{this.state.teachingBubbleStep === 4 && (
<TeachingBubble
headline={t(Keys.panes.addCollection.teachingBubble.step4Headline)}
headline="Setting partition key"
target={"#addCollection-partitionKeyValue"}
calloutProps={{ gapSpace: 16 }}
primaryButtonProps={{
text: t(Keys.panes.addCollection.teachingBubble.step4CreateContainer),
text: "Create container",
onClick: () => {
this.setState({ teachingBubbleStep: 5 });
this.submit();
},
}}
secondaryButtonProps={{
text: t(Keys.common.previous),
onClick: () => this.setState({ teachingBubbleStep: 2 }),
}}
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 2 }) }}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent={t(Keys.panes.addCollection.teachingBubble.stepOfTotal, { current: "4", total: "4" })}
footerContent="Step 4 of 4"
>
{t(Keys.panes.addCollection.teachingBubble.step4Body)}
Last step - you will need to define a partition key for your collection. /address was chosen for this
particular example. A good partition key should have a wide range of possible value
</TeachingBubble>
)}
@@ -279,23 +270,21 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{userContext.apiType === "Mongo"
? t(Keys.panes.addCollection.databaseFieldLabelName)
: t(Keys.panes.addCollection.databaseFieldLabelId)}
Database {userContext.apiType === "Mongo" ? "name" : "id"}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.databaseTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.databaseTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
/>
</TooltipHost>
</Stack>
@@ -306,7 +295,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={this.state.createNewDatabase}
aria-label={t(Keys.panes.addCollection.createNewDatabaseAriaLabel)}
aria-label="Create new database"
aria-checked={this.state.createNewDatabase}
name="databaseType"
type="radio"
@@ -315,12 +304,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
checked={!this.state.createNewDatabase}
aria-label={t(Keys.panes.addCollection.useExistingDatabaseAriaLabel)}
aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase}
name="databaseType"
type="radio"
@@ -328,7 +317,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack>
)}
@@ -344,10 +333,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.addCollection.newDatabaseIdPlaceholder)}
placeholder="Type a new database id"
size={40}
className="panelTextField"
aria-label={t(Keys.panes.addCollection.newDatabaseIdAriaLabel)}
aria-label="New database id, Type a new database id"
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -358,9 +347,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={t(Keys.panes.addCollection.shareThroughput, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -378,17 +365,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.shareThroughputTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.shareThroughputTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
/>
</TooltipHost>
</Stack>
@@ -413,10 +400,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!this.state.createNewDatabase && (
<Dropdown
ariaLabel={t(Keys.panes.addCollection.chooseExistingDatabase)}
ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder={t(Keys.panes.addCollection.chooseExistingDatabase)}
placeholder="Choose an existing database"
options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string })
@@ -437,18 +424,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
/>
</TooltipHost>
</Stack>
@@ -462,10 +445,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.addCollection.collectionIdPlaceholder, { collectionName: getCollectionName() })}
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"
aria-label={t(Keys.panes.addCollection.collectionIdAriaLabel, { collectionName: getCollectionName() })}
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
value={this.state.collectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ collectionId: event.target.value })
@@ -479,7 +462,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.indexing)}
Indexing
</Text>
</Stack>
@@ -487,32 +470,32 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={this.state.enableIndexing}
aria-label={t(Keys.panes.addCollection.turnOnIndexing)}
aria-label="Turn on indexing"
aria-checked={this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOnIndexing.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.automatic)}</span>
<span className="panelRadioBtnLabel">Automatic</span>
<input
className="panelRadioBtn"
checked={!this.state.enableIndexing}
aria-label={t(Keys.panes.addCollection.turnOffIndexing)}
aria-label="Turn off indexing"
aria-checked={!this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOffIndexing.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
<span className="panelRadioBtnLabel">Off</span>
</Stack>
<Text variant="small">
{this.getFreeTierIndexingText()}{" "}
<Link target="_blank" href="https://aka.ms/cosmos-indexing-policy">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
</Stack>
@@ -525,17 +508,21 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.sharding)}
Sharding
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.shardingTooltip)}
content={
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.shardingTooltip)}
ariaLabel={
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
/>
</TooltipHost>
</Stack>
@@ -544,7 +531,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={!this.state.isSharded}
aria-label={t(Keys.panes.addCollection.unsharded)}
aria-label="Unsharded"
aria-checked={!this.state.isSharded}
name="unsharded"
type="radio"
@@ -553,12 +540,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onUnshardedRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.unshardedLabel)}</span>
<span className="panelRadioBtnLabel">Unsharded (20GB limit)</span>
<input
className="panelRadioBtn"
checked={this.state.isSharded}
aria-label={t(Keys.panes.addCollection.sharded)}
aria-label="Sharded"
aria-checked={this.state.isSharded}
name="sharded"
type="radio"
@@ -567,7 +554,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onShardedRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.sharded)}</span>
<span className="panelRadioBtnLabel">Sharded</span>
</Stack>
</Stack>
)}
@@ -692,14 +679,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
>
{t(Keys.panes.addCollection.addPartitionKey)}
Add hierarchical partition key
</DefaultButton>
{this.state.subPartitionKeys.length > 0 && (
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET
V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
)}
@@ -712,9 +700,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
checked={this.state.enableDedicatedThroughput}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -732,19 +718,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true,
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true,
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
/>
</TooltipHost>
</Stack>
@@ -779,8 +769,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
placeholder={
userContext.apiType === "Mongo"
? t(Keys.panes.addCollection.uniqueKeysPlaceholderMongo)
: t(Keys.panes.addCollection.uniqueKeysPlaceholderSql)
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
value={uniqueKey}
@@ -812,7 +802,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
styles={{ root: { padding: 0 }, label: { fontSize: 12, color: "var(--colorNeutralForeground1)" } }}
onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })}
>
{t(Keys.panes.addCollection.addUniqueKey)}
Add unique key
</ActionButton>
</Stack>
)}
@@ -833,7 +823,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
@@ -842,13 +832,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.on)}</span>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
@@ -857,29 +847,27 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}{" "}
<br />
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
<DefaultButton
text={t(Keys.panes.addCollection.enable)}
onClick={() => this.props.explorer.openEnableSynapseLinkDialog(this.props.targetAccountOverride)}
text="Enable"
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
/>
@@ -890,7 +878,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowVectorSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.containerVectorPolicy)}
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent");
@@ -918,7 +906,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowFullTextSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.containerFullTextSearchPolicy)}
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
@@ -947,7 +935,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!isFabricNative() && userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.advanced)}
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
@@ -960,23 +948,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.indexing)}
Indexing
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.mongoIndexingTooltip)}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.mongoIndexingTooltip)}
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
/>
</TooltipHost>
</Stack>
<Checkbox
label={t(Keys.panes.addCollection.createWildcardIndex)}
label="Create a Wildcard Index on all fields"
checked={this.state.createMongoWildCardIndex}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -998,7 +986,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<Checkbox
label={t(Keys.panes.addCollection.legacySdkCheckbox)}
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
checked={this.state.useHashV1}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -1015,9 +1003,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
/>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" /> {t(Keys.panes.addCollection.legacySdkInfo)}{" "}
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the
created container will use a legacy partitioning scheme that supports partition key values of size
only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
</Stack>
@@ -1028,7 +1018,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div>
{!this.props.isCopyJobFlow && (
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={this.state.isThroughputCapExceeded} />
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
)}
{this.state.isExecuting && (
@@ -1036,15 +1026,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<PanelLoadingScreen />
{this.state.teachingBubbleStep === 5 && (
<TeachingBubble
headline={t(Keys.panes.addCollection.teachingBubble.step5Headline)}
headline="Creating sample container"
target={"#loadingScreen"}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
styles={{ footer: { width: "100%" } }}
>
{t(Keys.panes.addCollection.teachingBubble.step5Body)}
A sample container is now being created and we are adding sample data for you. It should take about 1
minute.
<br />
<br />
{t(Keys.panes.addCollection.teachingBubble.step5BodyFollowUp)}
Once the sample container is created, review your sample dataset and follow next steps
<br />
<br />
<ProgressIndicator
@@ -1053,7 +1044,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
progressTrack: { backgroundColor: "#A6A6A6" },
progressBar: { background: "white" },
}}
label={t(Keys.panes.addCollection.addingSampleDataSet)}
label="Adding sample data set"
/>
</TeachingBubble>
)}
@@ -1064,9 +1055,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private getDatabaseOptions(): IDropdownOption[] {
if (this.props.externalDatabaseOptions) {
return this.props.externalDatabaseOptions;
}
return useDatabases.getState().databases?.map((database) => ({
key: database.id(),
text: database.id(),
@@ -1154,10 +1142,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
if (this.props.targetAccountOverride) {
return false;
}
const selectedDatabase = useDatabases
.getState()
.databases?.find((database) => database.id() === this.state.selectedDatabaseId);
@@ -1166,8 +1150,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
private getFreeTierIndexingText(): string {
return this.state.enableIndexing
? t(Keys.panes.addCollection.indexingOnInfo)
: t(Keys.panes.addCollection.indexingOffInfo);
? "All properties in your documents will be indexed by default for flexible and efficient queries."
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
}
private getPartitionKeySubtext(): string {
@@ -1265,14 +1249,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
const throughput = this.state.createNewDatabase ? this.newDatabaseThroughput : this.collectionThroughput;
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.isCostAcknowledged) {
const errorMessage = this.isNewDatabaseAutoscale
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
this.setState({ errorMessage });
return false;
}
if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) {
this.setState({ errorMessage: t(Keys.panes.addCollection.unshardedMaxRuError) });
this.setState({ errorMessage: "Unsharded collections support up to 10,000 RUs" });
return false;
}
@@ -1286,12 +1270,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
if (this.shouldShowVectorSearchParameters()) {
if (!this.state.vectorPolicyValidated) {
this.setState({ errorMessage: t(Keys.panes.addCollection.vectorPolicyError) });
this.setState({ errorMessage: "Please fix errors in container vector policy" });
return false;
}
if (!this.state.fullTextPolicyValidated) {
this.setState({ errorMessage: t(Keys.panes.addCollection.fullTextSearchPolicyError) });
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
return false;
}
}
@@ -1448,16 +1432,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
vectorEmbeddingPolicy,
fullTextPolicy: this.state.fullTextPolicy,
targetAccountOverride: this.props.targetAccountOverride,
};
this.setState({ isExecuting: true });
try {
await createCollection(createCollectionParams);
if (!this.props.isCopyJobFlow) {
await this.props.explorer.refreshAllDatabases();
}
await this.props.explorer.refreshAllDatabases();
if (this.props.isQuickstart) {
const database = useDatabases.getState().findDatabaseWithId(databaseId);
if (database) {
@@ -1486,13 +1467,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
useSidePanel.getState().closeSidePanel();
}
} catch (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;
const errorMessage: string = getErrorMessage(error);
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
TelemetryProcessor.traceFailure(Action.CreateCollection, failureTelemetryData, startKey);

View File

@@ -3,32 +3,28 @@ import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { Keys, t } from "Localization";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { userContext } from "UserContext";
export function getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
return t(Keys.panes.addCollectionUtility.shardKeyTooltip);
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = t(Keys.panes.addCollectionUtility.partitionKeyTooltip, {
partitionKeyName: getPartitionKeyName(true),
});
let tooltipText = `The ${getPartitionKeyName(
true,
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
if (userContext.apiType === "SQL") {
tooltipText += t(Keys.panes.addCollectionUtility.partitionKeyTooltipSqlSuffix);
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
export function getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName =
userContext.apiType === "Mongo"
? t(Keys.panes.addCollectionUtility.shardKeyLabel)
: t(Keys.panes.addCollectionUtility.partitionKeyLabel);
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
@@ -36,19 +32,19 @@ export function getPartitionKeyName(isLowerCase?: boolean): string {
export function getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
return t(Keys.panes.addCollectionUtility.shardKeyPlaceholder);
return "e.g., categoryId";
case "Gremlin":
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderDefault);
return "e.g., /address";
case "SQL":
return `${
index === undefined
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderFirst)
? "Required - first partition key e.g., /TenantId"
: index === 0
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderSecond)
: t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderThird)
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderGraph);
return "e.g., /address/zipCode";
}
}
@@ -73,12 +69,13 @@ export function isFreeTierAccount(): boolean {
}
export function UniqueKeysHeader(): JSX.Element {
const tooltipContent = t(Keys.panes.addCollectionUtility.uniqueKeysTooltip);
const tooltipContent =
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollectionUtility.uniqueKeysLabel)}
Unique keys
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
@@ -102,11 +99,12 @@ export function shouldShowAnalyticalStoreOptions(): boolean {
}
export function AnalyticalStoreHeader(): JSX.Element {
const tooltipContent = t(Keys.panes.addCollectionUtility.analyticalStoreTooltip);
const tooltipContent =
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollectionUtility.analyticalStoreLabel)}
Analytical Store
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
@@ -118,13 +116,14 @@ export function AnalyticalStoreHeader(): JSX.Element {
export function AnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
{t(Keys.panes.addCollectionUtility.analyticalStoreDescription)}{" "}
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
);
@@ -156,9 +155,10 @@ export function scrollToSection(id: string): void {
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
{t(Keys.panes.addCollectionUtility.vectorPolicyTooltip)}{" "}
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
);

View File

@@ -29,7 +29,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
className="panelTextBold"
variant="small"
>
Database id
Database
id
</Text>
<StyledTooltipHostBase
content="A database is analogous to a namespace. It is the unit of management for a set of containers."
@@ -481,8 +482,10 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
}
variant="small"
>
Azure Synapse Link is required for creating an analytical store container. Enable Synapse Link for this Cosmos DB account.
Azure Synapse Link is required for creating an analytical store
container
. Enable Synapse Link for this Cosmos DB account.
<br />
<StyledLinkBase
aria-label="Learn more about Azure Synapse Link."
@@ -605,8 +608,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
className="removeIcon"
iconName="InfoSolid"
/>
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
<StyledLinkBase
href="https://aka.ms/cosmos-large-pk"

View File

@@ -1,6 +1,5 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { Keys, t } from "Localization";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
@@ -41,18 +40,15 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
const databaseIdLabel: string = isCassandraAccount
? t(Keys.panes.addDatabase.keyspaceIdLabel)
: t(Keys.panes.addDatabase.databaseIdLabel);
const databaseIdPlaceHolder: string = t(Keys.panes.addDatabase.databaseIdPlaceholder, { databaseLabel });
const databaseIdLabel: string = isCassandraAccount ? "Keyspace id" : "Database id";
const databaseIdPlaceHolder: string = isCassandraAccount ? "Type a new keyspace id" : "Type a new database id";
const [databaseId, setDatabaseId] = useState<string>("");
const databaseIdTooltipText = t(Keys.panes.addDatabase.databaseTooltip, { databaseLabel, collectionsLabel });
const databaseIdTooltipText = `A ${
isCassandraAccount ? "keyspace" : "database"
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
const databaseLevelThroughputTooltipText = t(Keys.panes.addDatabase.shareThroughputTooltip, {
databaseLabel,
collectionsLabel,
});
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
getNewDatabaseSharedThroughputDefault(),
);
@@ -148,17 +144,15 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
// TODO add feature flag that disables validation for customers with custom accounts
if (isAutoscaleSelected) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
setFormErrors(t(Keys.panes.addDatabase.greaterThanError, { minValue: AutoPilotUtils.autoPilotThroughput1K }));
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`,
);
return false;
}
}
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
setFormErrors(
isAutoscaleSelected
? t(Keys.panes.addDatabase.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addDatabase.acknowledgeSpendErrorDaily),
);
setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`);
return false;
}
@@ -175,7 +169,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
const props: RightPaneFormProps = {
formError: formErrors,
isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -193,7 +187,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
linkText={t(Keys.common.learnMore)}
linkText="Learn more"
/>
)}
<div className="panelMainContent">
@@ -228,7 +222,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
title={t(Keys.panes.addDatabase.provisionSharedThroughputTitle)}
title="Provision shared throughput"
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
checkbox: { width: 12, height: 12 },
@@ -239,7 +233,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
},
},
}}
label={t(Keys.panes.addDatabase.provisionThroughputLabel)}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/>

View File

@@ -40,7 +40,6 @@ import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent"
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel";
import { Keys, t } from "Localization";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -169,19 +168,19 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
}
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage: string = t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly);
const errorMessage: string = "Please acknowledge the estimated monthly spend.";
setErrorMessage(errorMessage);
return false;
}
if (showVectorSearchParameters()) {
if (!vectorPolicyValidated) {
setErrorMessage(t(Keys.panes.addCollection.vectorPolicyError));
setErrorMessage("Please fix errors in container vector policy");
return false;
}
if (!fullTextPolicyValidated) {
setErrorMessage(t(Keys.panes.addCollection.fullTextSearchPolicyError));
setErrorMessage("Please fix errors in container full text search policy");
return false;
}
}
@@ -308,7 +307,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexId)}
Global secondary index container id
</Text>
</Stack>
<input
@@ -319,7 +318,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexIdPlaceholder)}
placeholder={`e.g., indexbyEmailId`}
size={40}
className="panelTextField"
value={globalSecondaryIndexId}
@@ -337,7 +336,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank"
>
{t(Keys.panes.addGlobalSecondaryIndex.projectionQueryTooltip)}
Learn more about defining global secondary indexes.
</Link>
}
>
@@ -350,7 +349,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
aria-required
required
autoComplete="off"
placeholder={t(Keys.panes.addGlobalSecondaryIndex.projectionQueryPlaceholder)}
placeholder={"SELECT c.email, c.accountId FROM c"}
size={40}
className="panelTextField"
value={definition || ""}
@@ -394,7 +393,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
</Stack>
</div>
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={isThroughputCapExceeded} />
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
{isExecuting && <PanelLoadingScreen />}
</form>
);

View File

@@ -146,7 +146,6 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
explorer={
Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],

View File

@@ -2,15 +2,14 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
import * as Constants from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import { Keys, t } from "Localization";
import React, { FunctionComponent, useState } from "react";
import * as SharedConstants from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
@@ -72,8 +71,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage =
isNewKeySpaceAutoscale || isTableAutoscale
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
setFormError(errorMessage);
return;
}
@@ -150,7 +149,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -162,8 +161,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.cassandraAddCollection.keyspaceLabel)}{" "}
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.keyspaceTooltip)}</InfoTooltip>
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</InfoTooltip>
</Text>
</Stack>
@@ -181,7 +179,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
setExistingKeyspaceId("");
}}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
@@ -195,7 +193,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
setIsKeyspaceShared(false);
}}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
<span className="panelRadioBtnLabel">Use existing</span>
</Stack>
{keyspaceCreateNew && (
@@ -277,9 +275,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.cassandraAddCollection.tableIdLabel)}{" "}
Enter CQL command to create the table.{" "}
<Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
{t(Keys.common.learnMore)}
Learn More
</Link>
</Text>
</Stack>
@@ -297,7 +295,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.cassandraAddCollection.enterTableId)}
placeholder="Enter table Id"
size={20}
value={tableId}
onChange={(e, newValue) => setTableId(newValue)}
@@ -309,7 +307,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
multiline
id="editor-area"
rows={5}
ariaLabel={t(Keys.panes.cassandraAddCollection.tableSchemaAriaLabel)}
ariaLabel="Table schema"
value={userTableQuery}
onChange={(e, newValue) => setUserTableQuery(newValue)}
/>
@@ -320,12 +318,17 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<input
type="checkbox"
id="tableSharedThroughput"
title={t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}
title="Provision dedicated throughput for this table"
checked={dedicateTableThroughput}
onChange={(e) => setDedicateTableThroughput(e.target.checked)}
/>
<span>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}</span>
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughputTooltip)}</InfoTooltip>
<span>Provision dedicated throughput for this table</span>
<InfoTooltip>
You can optionally provision dedicated throughput for a table within a keyspace that has throughput
provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
billed in addition to the throughput amount you provisioned at the keyspace level.
</InfoTooltip>
</Stack>
)}
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (

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