mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-04 16:51:19 +00:00
Compare commits
2 Commits
ashleyst/r
...
users/sevo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64865720d3 | ||
|
|
3194ff2271 |
@@ -1,16 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm
|
|
||||||
|
|
||||||
# Install pre-reqs for gyp, and 'canvas' npm module
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
make \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
python3-minimal \
|
|
||||||
libcairo2-dev \
|
|
||||||
libpango1.0-dev \
|
|
||||||
&& \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install node-gyp to build native modules
|
|
||||||
RUN npm install -g node-gyp
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
|
||||||
{
|
|
||||||
"name": "Azure Cosmos DB Explorer",
|
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
|
||||||
"build": {
|
|
||||||
"dockerfile": "Dockerfile"
|
|
||||||
},
|
|
||||||
"onCreateCommand": ".devcontainer/oncreate",
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/azure-cli:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
|
||||||
"installDirectlyFromGitHubRelease": true,
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/sshd:1": {
|
|
||||||
"version": "latest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
|
||||||
// "features": {},
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
// "postCreateCommand": "yarn install",
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
// "customizations": {},
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "root"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Install packages once, to prime the node_modules directory.
|
|
||||||
npm ci
|
|
||||||
65
.vscode/launch.json
vendored
65
.vscode/launch.json
vendored
@@ -4,6 +4,37 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "npm start",
|
||||||
|
"name": "Run Host (npm start)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "Debug in Portal (Chrome)",
|
||||||
|
"request": "launch",
|
||||||
|
"smartStep": true,
|
||||||
|
"url": "https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html",
|
||||||
|
"presentation": {
|
||||||
|
"group": "debug"
|
||||||
|
//"order": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "Debug Standalone (Chrome)",
|
||||||
|
"request": "launch",
|
||||||
|
"smartStep": true,
|
||||||
|
"url": "https://localhost:1234/hostedExplorer.html",
|
||||||
|
"presentation": {
|
||||||
|
"group": "debug"
|
||||||
|
//"order": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug Jest Tests",
|
"name": "Debug Jest Tests",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
@@ -17,7 +48,11 @@
|
|||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen",
|
"internalConsoleOptions": "neverOpen",
|
||||||
"port": 9229
|
"port": 9229,
|
||||||
|
"presentation": {
|
||||||
|
"group": "tests"
|
||||||
|
//"order": 5
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug Jest Current Test",
|
"name": "Debug Jest Current Test",
|
||||||
@@ -28,7 +63,7 @@
|
|||||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||||
"${fileBasenameNoExtension}",
|
"${fileBasenameNoExtension}",
|
||||||
"--coverage",
|
"--coverage",
|
||||||
"false",
|
"false"
|
||||||
// "--watch",
|
// "--watch",
|
||||||
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
|
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
|
||||||
// // https://github.com/facebook/jest/issues/6683
|
// // https://github.com/facebook/jest/issues/6683
|
||||||
@@ -36,7 +71,31 @@
|
|||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen",
|
"internalConsoleOptions": "neverOpen",
|
||||||
"port": 9229
|
"port": 9229,
|
||||||
|
"presentation": {
|
||||||
|
"group": "tests"
|
||||||
|
//"order": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Run Host and Debug in Portal (Chrome)",
|
||||||
|
"configurations": ["Run Host (npm start)", "Debug in Portal (Chrome)"],
|
||||||
|
"stopAll": true,
|
||||||
|
"presentation": {
|
||||||
|
"group": "debug",
|
||||||
|
"order": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run Host and Debug Standalone (Chrome)",
|
||||||
|
"configurations": ["Run Host (npm start)", "Debug Standalone (Chrome)"],
|
||||||
|
"stopAll": true,
|
||||||
|
"presentation": {
|
||||||
|
"group": "debug",
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
16
.vscode/tasks.json
vendored
Normal file
16
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "build",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"label": "npm: build",
|
||||||
|
"detail": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1906,14 +1906,8 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
height: 32px;
|
padding-top: 5px;
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navTabHeight {
|
.navTabHeight {
|
||||||
@@ -2618,7 +2612,6 @@ a:link {
|
|||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -2527,13 +2527,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": {
|
"node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": {
|
||||||
"version": "0.10.6",
|
"version": "0.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz",
|
||||||
"integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
|
"integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.2",
|
"@babel/helper-define-polyfill-provider": "^0.6.1",
|
||||||
"core-js-compat": "^3.38.0"
|
"core-js-compat": "^3.36.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
@@ -2932,10 +2932,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.6.2",
|
"version": "1.6.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/utils": "^0.2.0"
|
"@floating-ui/utils": "^0.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/devtools": {
|
"node_modules/@floating-ui/devtools": {
|
||||||
@@ -2945,15 +2945,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/dom": {
|
"node_modules/@floating-ui/dom": {
|
||||||
"version": "1.6.5",
|
"version": "1.6.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.0.0",
|
"@floating-ui/core": "^1.0.0",
|
||||||
"@floating-ui/utils": "^0.2.0"
|
"@floating-ui/utils": "^0.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@fluentui/date-time-utilities": {
|
"node_modules/@fluentui/date-time-utilities": {
|
||||||
@@ -3501,7 +3501,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.8.10.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.8.10.tgz",
|
||||||
"integrity": "sha512-Xvnn6uKMsinMg/zo79KBNCDABnl0gpmArQYNQya9FCNRzvmHUCDvuQCqv4IKslvPvuC0Ya8mR2NORm2w0JoZiw==",
|
"integrity": "sha512-Xvnn6uKMsinMg/zo79KBNCDABnl0gpmArQYNQya9FCNRzvmHUCDvuQCqv4IKslvPvuC0Ya8mR2NORm2w0JoZiw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-window-provider": "^2.2.28",
|
"@fluentui/react-window-provider": "^2.2.27",
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"@fluentui/utilities": "^8.15.13",
|
"@fluentui/utilities": "^8.15.13",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
@@ -4426,9 +4426,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fluentui/react-window-provider": {
|
"node_modules/@fluentui/react-window-provider": {
|
||||||
"version": "2.2.28",
|
"version": "2.2.27",
|
||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.27.tgz",
|
||||||
"integrity": "sha512-YdZ74HTaoDwlvLDzoBST80/17ExIl93tLJpTxnqK5jlJOAUVQ+mxLPF2HQEJq+SZr5IMXHsQ56w/KaZVRn72YA==",
|
"integrity": "sha512-Dg0G9bizjryV0Q/r0CPtCVTPa2II/EsT9E6JT3jPSALjQADDLlW4/+ZXbcEC7geZ/40+KpZDmhplvk/AJSFBKg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
@@ -4512,7 +4512,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/dom-utilities": "^2.3.7",
|
"@fluentui/dom-utilities": "^2.3.7",
|
||||||
"@fluentui/merge-styles": "^8.6.12",
|
"@fluentui/merge-styles": "^8.6.12",
|
||||||
"@fluentui/react-window-provider": "^2.2.28",
|
"@fluentui/react-window-provider": "^2.2.27",
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -14966,9 +14966,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.23.3",
|
"version": "4.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
|
||||||
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
|
"integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -14984,9 +14984,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001646",
|
"caniuse-lite": "^1.0.30001640",
|
||||||
"electron-to-chromium": "^1.5.4",
|
"electron-to-chromium": "^1.4.820",
|
||||||
"node-releases": "^2.0.18",
|
"node-releases": "^2.0.14",
|
||||||
"update-browserslist-db": "^1.1.0"
|
"update-browserslist-db": "^1.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -15142,9 +15142,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001651",
|
"version": "1.0.30001645",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz",
|
||||||
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
|
"integrity": "sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -16063,12 +16063,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.38.0",
|
"version": "3.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
||||||
"integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==",
|
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.23.3"
|
"browserslist": "^4.23.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|||||||
@@ -134,9 +134,6 @@ export class BackendApi {
|
|||||||
public static readonly GenerateToken: string = "GenerateToken";
|
public static readonly GenerateToken: string = "GenerateToken";
|
||||||
public static readonly PortalSettings: string = "PortalSettings";
|
public static readonly PortalSettings: string = "PortalSettings";
|
||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
||||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
|
||||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
|
||||||
public static readonly SampleData: string = "SampleData";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
@@ -186,12 +183,6 @@ export class CassandraProxyAPIs {
|
|||||||
public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema";
|
public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AadEndpoints {
|
|
||||||
public static readonly Prod: string = "https://login.microsoftonline.com/";
|
|
||||||
public static readonly Fairfax: string = "https://login.microsoftonline.us/";
|
|
||||||
public static readonly Mooncake: string = "https://login.partner.microsoftonline.cn/";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Queries {
|
export class Queries {
|
||||||
public static CustomPageOption: string = "custom";
|
public static CustomPageOption: string = "custom";
|
||||||
public static UnlimitedPageOption: string = "unlimited";
|
public static UnlimitedPageOption: string = "unlimited";
|
||||||
@@ -293,7 +284,6 @@ export class HttpStatusCodes {
|
|||||||
public static readonly Accepted: number = 202;
|
public static readonly Accepted: number = 202;
|
||||||
public static readonly NoContent: number = 204;
|
public static readonly NoContent: number = 204;
|
||||||
public static readonly NotModified: number = 304;
|
public static readonly NotModified: number = 304;
|
||||||
public static readonly BadRequest: number = 400;
|
|
||||||
public static readonly Unauthorized: number = 401;
|
public static readonly Unauthorized: number = 401;
|
||||||
public static readonly Forbidden: number = 403;
|
public static readonly Forbidden: number = 403;
|
||||||
public static readonly NotFound: number = 404;
|
public static readonly NotFound: number = 404;
|
||||||
@@ -505,7 +495,7 @@ export class PriorityLevel {
|
|||||||
public static readonly Default = "low";
|
public static readonly Default = "low";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
export const QueryCopilotSampleContainerSchema = {
|
export const QueryCopilotSampleContainerSchema = {
|
||||||
|
|||||||
@@ -3,16 +3,15 @@ import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizatio
|
|||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
import { getErrorMessage } from "./ErrorHandlingUtils";
|
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||||
|
import * as Logger from "../Common/Logger";
|
||||||
|
|
||||||
const _global = typeof self === "undefined" ? window : self;
|
const _global = typeof self === "undefined" ? window : self;
|
||||||
|
|
||||||
@@ -124,37 +123,6 @@ export async function getTokenFromAuthService(
|
|||||||
verb: string,
|
verb: string,
|
||||||
resourceType: string,
|
resourceType: string,
|
||||||
resourceId?: string,
|
resourceId?: string,
|
||||||
): Promise<AuthorizationToken> {
|
|
||||||
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
|
||||||
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
|
||||||
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
"x-ms-encrypted-auth-token": userContext.accessToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
verb,
|
|
||||||
resourceType,
|
|
||||||
resourceId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const result: AuthorizationToken = await response.json();
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTokenFromAuthService_ToBeDeprecated(
|
|
||||||
verb: string,
|
|
||||||
resourceType: string,
|
|
||||||
resourceId?: string,
|
|
||||||
): Promise<AuthorizationToken> {
|
): Promise<AuthorizationToken> {
|
||||||
try {
|
try {
|
||||||
const host = configContext.BACKEND_ENDPOINT;
|
const host = configContext.BACKEND_ENDPOINT;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { PortalBackendEndpoints } from "Common/Constants";
|
|
||||||
import { updateConfigContext } from "ConfigContext";
|
|
||||||
import * as EnvironmentUtility from "./EnvironmentUtility";
|
import * as EnvironmentUtility from "./EnvironmentUtility";
|
||||||
|
|
||||||
describe("Environment Utility Test", () => {
|
describe("Environment Utility Test", () => {
|
||||||
@@ -13,18 +11,4 @@ describe("Environment Utility Test", () => {
|
|||||||
const expectedResult = "test/";
|
const expectedResult = "test/";
|
||||||
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
|
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Detect environment is Mpac", () => {
|
|
||||||
updateConfigContext({
|
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
|
||||||
});
|
|
||||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Mpac);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Detect environment is Development", () => {
|
|
||||||
updateConfigContext({
|
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
|
|
||||||
});
|
|
||||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,29 +1,6 @@
|
|||||||
import { PortalBackendEndpoints } from "Common/Constants";
|
|
||||||
import { configContext } from "ConfigContext";
|
|
||||||
|
|
||||||
export function normalizeArmEndpoint(uri: string): string {
|
export function normalizeArmEndpoint(uri: string): string {
|
||||||
if (uri && uri.slice(-1) !== "/") {
|
if (uri && uri.slice(-1) !== "/") {
|
||||||
return `${uri}/`;
|
return `${uri}/`;
|
||||||
}
|
}
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Environment {
|
|
||||||
Development = "Development",
|
|
||||||
Mpac = "MPAC",
|
|
||||||
Prod = "Prod",
|
|
||||||
Fairfax = "Fairfax",
|
|
||||||
Mooncake = "Mooncake",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getEnvironment = (): Environment => {
|
|
||||||
const environmentMap: { [key: string]: Environment } = {
|
|
||||||
[PortalBackendEndpoints.Development]: Environment.Development,
|
|
||||||
[PortalBackendEndpoints.Mpac]: Environment.Mpac,
|
|
||||||
[PortalBackendEndpoints.Prod]: Environment.Prod,
|
|
||||||
[PortalBackendEndpoints.Fairfax]: Environment.Fairfax,
|
|
||||||
[PortalBackendEndpoints.Mooncake]: Environment.Mooncake,
|
|
||||||
};
|
|
||||||
|
|
||||||
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -550,49 +550,6 @@ export function deleteDocument_ToBeDeprecated(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteDocuments(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
documentIds: DocumentId[],
|
|
||||||
): Promise<{
|
|
||||||
deletedCount: number;
|
|
||||||
isAcknowledged: boolean;
|
|
||||||
}> {
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
|
||||||
|
|
||||||
const rids = documentIds.map((documentId) => documentId.id());
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
databaseID: databaseId,
|
|
||||||
collectionID: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}`,
|
|
||||||
resourceIDs: rids,
|
|
||||||
subscriptionID: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseAccountName: databaseAccount.name,
|
|
||||||
};
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
|
||||||
method: "DELETE",
|
|
||||||
body: JSON.stringify(params),
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
const result = await response.json();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "deleting documents", params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
@@ -720,22 +677,23 @@ export function useMongoProxyEndpoint(api: string): boolean {
|
|||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Local,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
MongoProxyEndpoints.Fairfax,
|
// MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
|
||||||
];
|
];
|
||||||
|
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||||
|
if (
|
||||||
|
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
|
||||||
|
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||||
|
) {
|
||||||
|
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
canAccessMongoProxy &&
|
||||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThrottlingError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
||||||
// It causes problems for TypeScript understanding the types
|
// It causes problems for TypeScript understanding the types
|
||||||
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
||||||
@@ -745,14 +703,6 @@ async function errorHandling(response: Response, action: string, params: unknown
|
|||||||
if (response.status === HttpStatusCodes.Forbidden) {
|
if (response.status === HttpStatusCodes.Forbidden) {
|
||||||
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
||||||
return;
|
return;
|
||||||
} else if (
|
|
||||||
response.status === HttpStatusCodes.BadRequest &&
|
|
||||||
errorMessage.includes("Error=16500") &&
|
|
||||||
errorMessage.includes("RetryAfterMs=")
|
|
||||||
) {
|
|
||||||
// If throttling is happening, Cosmos DB will return a 400 with a body of:
|
|
||||||
// A write operation resulted in an error. Error=16500, RetryAfterMs=4, Details='Batch write error.
|
|
||||||
throw new ThrottlingError(errorMessage);
|
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/Common/PortalNotifications.ts
Normal file
39
src/Common/PortalNotifications.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { configContext, Platform } from "../ConfigContext";
|
||||||
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
import { userContext } from "../UserContext";
|
||||||
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
|
|
||||||
|
const notificationsPath = () => {
|
||||||
|
switch (configContext.platform) {
|
||||||
|
case Platform.Hosted:
|
||||||
|
return "/api/guest/notifications";
|
||||||
|
case Platform.Portal:
|
||||||
|
return "/api/notifications";
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown platform: ${configContext.platform}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||||
|
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
|
||||||
|
const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${
|
||||||
|
databaseAccount.name
|
||||||
|
}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
||||||
|
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as DataModels.Notification[];
|
||||||
|
};
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import QueryError, { QueryErrorLocation, QueryErrorSeverity } from "Common/QueryError";
|
|
||||||
|
|
||||||
describe("QueryError.tryParse", () => {
|
|
||||||
const testErrorLocationResolver = ({ start, end }: { start: number; end: number }) =>
|
|
||||||
new QueryErrorLocation(
|
|
||||||
{ offset: start, lineNumber: start, column: start },
|
|
||||||
{ offset: end, lineNumber: end, column: end },
|
|
||||||
);
|
|
||||||
|
|
||||||
it("handles a string error", () => {
|
|
||||||
const error = "error";
|
|
||||||
const result = QueryError.tryParse(error, testErrorLocationResolver);
|
|
||||||
expect(result).toEqual([new QueryError("error", QueryErrorSeverity.Error)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles an error object", () => {
|
|
||||||
const error = {
|
|
||||||
message: "error",
|
|
||||||
severity: "Warning",
|
|
||||||
location: { start: 0, end: 1 },
|
|
||||||
code: "code",
|
|
||||||
};
|
|
||||||
const result = QueryError.tryParse(error, testErrorLocationResolver);
|
|
||||||
expect(result).toEqual([
|
|
||||||
new QueryError(
|
|
||||||
"error",
|
|
||||||
QueryErrorSeverity.Warning,
|
|
||||||
"code",
|
|
||||||
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles a JSON message without syntax errors", () => {
|
|
||||||
const innerError = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message: "Your query is bad, and you should feel bad",
|
|
||||||
};
|
|
||||||
const message = JSON.stringify(innerError);
|
|
||||||
const outerError = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
|
||||||
expect(result).toEqual([
|
|
||||||
new QueryError("Your query is bad, and you should feel bad", QueryErrorSeverity.Error, "BadRequest"),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message.
|
|
||||||
it("handles single-nested error", () => {
|
|
||||||
const errors = [
|
|
||||||
{
|
|
||||||
message: "error1",
|
|
||||||
severity: "Warning",
|
|
||||||
location: { start: 0, end: 1 },
|
|
||||||
code: "code1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "error2",
|
|
||||||
severity: "Error",
|
|
||||||
location: { start: 2, end: 3 },
|
|
||||||
code: "code2",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const innerError = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message: "Your query is bad, and you should feel bad",
|
|
||||||
errors,
|
|
||||||
};
|
|
||||||
const message = JSON.stringify(innerError);
|
|
||||||
const outerError = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
|
||||||
expect(result).toEqual([
|
|
||||||
new QueryError(
|
|
||||||
"error1",
|
|
||||||
QueryErrorSeverity.Warning,
|
|
||||||
"code1",
|
|
||||||
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
|
|
||||||
),
|
|
||||||
new QueryError(
|
|
||||||
"error2",
|
|
||||||
QueryErrorSeverity.Error,
|
|
||||||
"code2",
|
|
||||||
new QueryErrorLocation({ offset: 2, lineNumber: 2, column: 2 }, { offset: 3, lineNumber: 3, column: 3 }),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { getErrorMessage } from "Common/ErrorHandlingUtils";
|
||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
|
||||||
|
|
||||||
export enum QueryErrorSeverity {
|
export enum QueryErrorSeverity {
|
||||||
Error = "Error",
|
Error = "Error",
|
||||||
@@ -97,44 +97,13 @@ export const createMonacoMarkersForQueryErrors = (errors: QueryError[]) => {
|
|||||||
.filter((marker) => !!marker);
|
.filter((marker) => !!marker);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ErrorEnrichment {
|
|
||||||
title?: string;
|
|
||||||
message: string;
|
|
||||||
learnMoreUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {
|
|
||||||
OPERATION_RU_LIMIT_EXCEEDED: (original) => {
|
|
||||||
if (ruThresholdEnabled()) {
|
|
||||||
const threshold = getRUThreshold();
|
|
||||||
return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`;
|
|
||||||
}
|
|
||||||
return original;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const HELP_LINKS: Record<string, string> = {
|
|
||||||
OPERATION_RU_LIMIT_EXCEEDED:
|
|
||||||
"https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class QueryError {
|
export default class QueryError {
|
||||||
message: string;
|
|
||||||
helpLink?: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
public message: string,
|
||||||
public severity: QueryErrorSeverity,
|
public severity: QueryErrorSeverity,
|
||||||
public code?: string,
|
public code?: string,
|
||||||
public location?: QueryErrorLocation,
|
public location?: QueryErrorLocation,
|
||||||
helpLink?: string,
|
) {}
|
||||||
) {
|
|
||||||
// Automatically replace the message with a more Data Explorer-specific message if we have for this error code.
|
|
||||||
this.message = REPLACEMENT_MESSAGES[code] ? REPLACEMENT_MESSAGES[code](message) : message;
|
|
||||||
|
|
||||||
// Automatically set the help link if we have one for this error code.
|
|
||||||
this.helpLink = helpLink ?? HELP_LINKS[code];
|
|
||||||
}
|
|
||||||
|
|
||||||
getMonacoSeverity(): monaco.MarkerSeverity {
|
getMonacoSeverity(): monaco.MarkerSeverity {
|
||||||
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
|
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
|
||||||
@@ -166,7 +135,7 @@ export default class QueryError {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = error as string;
|
const errorMessage = getErrorMessage(error as string | Error);
|
||||||
|
|
||||||
// Map some well known messages to richer errors
|
// Map some well known messages to richer errors
|
||||||
const knownError = knownErrors[errorMessage];
|
const knownError = knownErrors[errorMessage];
|
||||||
@@ -191,9 +160,7 @@ export default class QueryError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const severity =
|
const severity =
|
||||||
"severity" in error && typeof error.severity === "string"
|
"severity" in error && typeof error.severity === "string" ? (error.severity as QueryErrorSeverity) : undefined;
|
||||||
? (error.severity as QueryErrorSeverity)
|
|
||||||
: QueryErrorSeverity.Error;
|
|
||||||
const location =
|
const location =
|
||||||
"location" in error && typeof error.location === "object"
|
"location" in error && typeof error.location === "object"
|
||||||
? locationResolver(error.location as { start: number; end: number })
|
? locationResolver(error.location as { start: number; end: number })
|
||||||
@@ -206,15 +173,16 @@ export default class QueryError {
|
|||||||
error: unknown,
|
error: unknown,
|
||||||
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
||||||
): QueryError[] | null {
|
): QueryError[] | null {
|
||||||
let message: string | undefined;
|
if (typeof error === "object" && "message" in error) {
|
||||||
if (typeof error === "object" && "message" in error && typeof error.message === "string") {
|
error = error.message;
|
||||||
message = error.message;
|
}
|
||||||
} else {
|
|
||||||
// Unsupported error format.
|
if (typeof error !== "string") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
||||||
|
let message = error;
|
||||||
if (message.startsWith("Message: ")) {
|
if (message.startsWith("Message: ")) {
|
||||||
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
||||||
// So we use a separate variable to avoid this.
|
// So we use a separate variable to avoid this.
|
||||||
@@ -228,16 +196,13 @@ export default class QueryError {
|
|||||||
try {
|
try {
|
||||||
parsed = JSON.parse(message);
|
parsed = JSON.parse(message);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// The message doesn't contain a nested error.
|
// Not a query error.
|
||||||
return [QueryError.read(error, locationResolver)];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof parsed === "object") {
|
if (typeof parsed === "object" && "errors" in parsed && Array.isArray(parsed.errors)) {
|
||||||
if ("errors" in parsed && Array.isArray(parsed.errors)) {
|
|
||||||
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
|
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
|
||||||
}
|
}
|
||||||
return [QueryError.read(parsed, locationResolver)];
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import * as React from "react";
|
|||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
children: string;
|
children: string;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
|
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }: TooltipProps) => {
|
||||||
return (
|
return (
|
||||||
<span className={className}>
|
<span>
|
||||||
<TooltipHost content={children}>
|
<TooltipHost content={children}>
|
||||||
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
|
|||||||
@@ -26,23 +26,14 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IBulkDeleteResult {
|
|
||||||
documentId: DocumentId;
|
|
||||||
requestCharge: number;
|
|
||||||
statusCode: number;
|
|
||||||
retryAfterMilliseconds?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk delete documents
|
* Bulk delete documents
|
||||||
* @param collection
|
* @param collection
|
||||||
* @param documentId
|
* @param documentId
|
||||||
* @returns array of results and status codes
|
* @returns array of ids that were successfully deleted
|
||||||
*/
|
*/
|
||||||
export const deleteDocuments = async (
|
export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
|
||||||
collection: CollectionBase,
|
const nbDocuments = documentIds.length;
|
||||||
documentIds: DocumentId[],
|
|
||||||
): Promise<IBulkDeleteResult[]> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||||
try {
|
try {
|
||||||
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
||||||
@@ -65,17 +56,18 @@ export const deleteDocuments = async (
|
|||||||
operationType: BulkOperationType.Delete,
|
operationType: BulkOperationType.Delete,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
|
const promise = v2Container.items.bulk(operations).then((bulkResult) => {
|
||||||
return bulkResults.map((bulkResult, index) => {
|
return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
|
||||||
const documentId = documentIdsChunk[index];
|
|
||||||
return { ...bulkResult, documentId };
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
promiseArray.push(promise);
|
promiseArray.push(promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allResult = await Promise.all(promiseArray);
|
const allResult = await Promise.all(promiseArray);
|
||||||
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
||||||
|
logConsoleInfo(
|
||||||
|
`Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`,
|
||||||
|
);
|
||||||
|
// TODO: handle case result.length != nbDocuments
|
||||||
return flatAllResult;
|
return flatAllResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
|
|||||||
@@ -49,12 +49,14 @@ export interface ConfigContext {
|
|||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
PORTAL_BACKEND_ENDPOINT?: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
NEW_BACKEND_APIS?: BackendApi[];
|
||||||
MONGO_BACKEND_ENDPOINT?: string;
|
MONGO_BACKEND_ENDPOINT?: string;
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_PROXY_ENDPOINT?: string;
|
||||||
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
|
||||||
NEW_MONGO_APIS?: string[];
|
NEW_MONGO_APIS?: string[];
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
CASSANDRA_PROXY_ENDPOINT?: string;
|
||||||
|
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean;
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
NEW_CASSANDRA_APIS?: string[];
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
@@ -85,7 +87,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||||
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
@@ -115,10 +117,11 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
"deleteDocument",
|
"deleteDocument",
|
||||||
"createCollectionWithProxy",
|
"createCollectionWithProxy",
|
||||||
"legacyMongoShell",
|
"legacyMongoShell",
|
||||||
// "bulkdelete",
|
|
||||||
],
|
],
|
||||||
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||||
|
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export interface Database extends TreeNode {
|
|||||||
openAddCollection(database: Database, event: MouseEvent): void;
|
openAddCollection(database: Database, event: MouseEvent): void;
|
||||||
onSettingsClick: () => void;
|
onSettingsClick: () => void;
|
||||||
loadOffer(): Promise<void>;
|
loadOffer(): Promise<void>;
|
||||||
|
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionBase extends TreeNode {
|
export interface CollectionBase extends TreeNode {
|
||||||
@@ -190,6 +191,8 @@ export interface Collection extends CollectionBase {
|
|||||||
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
||||||
|
|
||||||
|
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* React component for Command button component.
|
* React component for Command button component.
|
||||||
*/
|
*/
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||||
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import * as StringUtils from "../../../Utils/StringUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for this component
|
* Options for this component
|
||||||
*/
|
*/
|
||||||
export interface CommandButtonComponentProps {
|
export interface CommandButtonComponentProps {
|
||||||
|
/**
|
||||||
|
* font icon name for the button
|
||||||
|
*/
|
||||||
|
iconName?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* image source for the button icon
|
* image source for the button icon
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +31,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent, container: Explorer) => void;
|
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
@@ -111,3 +120,157 @@ export interface CommandButtonComponentProps {
|
|||||||
*/
|
*/
|
||||||
keyboardAction?: KeyboardAction;
|
keyboardAction?: KeyboardAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||||
|
private dropdownElt: HTMLElement;
|
||||||
|
private expandButtonElt: HTMLElement;
|
||||||
|
|
||||||
|
public componentDidUpdate(): void {
|
||||||
|
if (!this.dropdownElt || !this.expandButtonElt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyPress(event: React.KeyboardEvent): boolean {
|
||||||
|
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
|
||||||
|
this.commandClickCallback && this.commandClickCallback(event);
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean {
|
||||||
|
if (event.keyCode === KeyCodes.DownArrow) {
|
||||||
|
$(this.dropdownElt).hide();
|
||||||
|
$(this.dropdownElt).show().focus();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.keyCode === KeyCodes.UpArrow) {
|
||||||
|
$(this.dropdownElt).hide();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCommandButtonId(): string {
|
||||||
|
if (this.props.id) {
|
||||||
|
return this.props.id;
|
||||||
|
} else {
|
||||||
|
return `commandButton-${StringUtils.stripSpacesFromString(this.props.commandButtonLabel)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static renderButton(options: CommandButtonComponentProps, key?: string): JSX.Element {
|
||||||
|
return <CommandButtonComponent key={key} {...options} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
private commandClickCallback(e: React.SyntheticEvent): void {
|
||||||
|
if (this.props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Query component's parent, not document
|
||||||
|
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
|
||||||
|
if (el) {
|
||||||
|
el.style.display = "none";
|
||||||
|
}
|
||||||
|
this.props.onCommandClick(e);
|
||||||
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
|
commandButtonClicked: this.props.commandButtonLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderChildren(): JSX.Element {
|
||||||
|
if (!this.props.children || this.props.children.length < 1) {
|
||||||
|
return <React.Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="commandExpand"
|
||||||
|
tabIndex={0}
|
||||||
|
ref={(ref: HTMLElement) => {
|
||||||
|
this.expandButtonElt = ref;
|
||||||
|
}}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)}
|
||||||
|
>
|
||||||
|
<div className="commandDropdownLauncher">
|
||||||
|
<span className="partialSplitter" />
|
||||||
|
<span className="expandDropdown">
|
||||||
|
<img src={CollapseChevronDownIcon} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="commandDropdownContainer"
|
||||||
|
ref={(ref: HTMLElement) => {
|
||||||
|
this.dropdownElt = ref;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="commandDropdown">
|
||||||
|
{this.props.children.map((c: CommandButtonComponentProps, index: number): JSX.Element => {
|
||||||
|
return CommandButtonComponent.renderButton(c, `${index}`);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static renderLabel(
|
||||||
|
props: CommandButtonComponentProps,
|
||||||
|
key?: string,
|
||||||
|
refct?: (input: HTMLElement) => void,
|
||||||
|
): JSX.Element {
|
||||||
|
if (!props.commandButtonLabel) {
|
||||||
|
return <React.Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="commandLabel" key={key} ref={refct}>
|
||||||
|
{props.commandButtonLabel}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
let mainClassName = "commandButtonComponent";
|
||||||
|
if (this.props.disabled) {
|
||||||
|
mainClassName += " commandDisabled";
|
||||||
|
}
|
||||||
|
if (this.props.isSelected) {
|
||||||
|
mainClassName += " selectedButton";
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentClassName = "commandContent";
|
||||||
|
if (this.props.children && this.props.children.length > 0) {
|
||||||
|
contentClassName += " hasHiddenItems";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="commandButtonReact">
|
||||||
|
<span
|
||||||
|
className={mainClassName}
|
||||||
|
role="menuitem"
|
||||||
|
tabIndex={this.props.tabIndex}
|
||||||
|
onKeyPress={(e: React.KeyboardEvent<HTMLSpanElement>) => this.onKeyPress(e)}
|
||||||
|
title={this.props.tooltipText}
|
||||||
|
id={this.getCommandButtonId()}
|
||||||
|
aria-disabled={this.props.disabled}
|
||||||
|
aria-haspopup={this.props.hasPopup}
|
||||||
|
aria-label={this.props.ariaLabel}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLSpanElement>) => this.commandClickCallback(e)}
|
||||||
|
>
|
||||||
|
<div className={contentClassName}>
|
||||||
|
<img className="commandIcon" src={this.props.iconSrc} alt={this.props.iconAlt} />
|
||||||
|
{CommandButtonComponent.renderLabel(this.props)}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
{this.props.children && this.renderChildren()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface DialogState {
|
|||||||
textFieldProps?: TextFieldProps,
|
textFieldProps?: TextFieldProps,
|
||||||
primaryButtonDisabled?: boolean,
|
primaryButtonDisabled?: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void;
|
showOkModalDialog: (title: string, subText: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||||
@@ -83,7 +83,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
textFieldProps,
|
textFieldProps,
|
||||||
primaryButtonDisabled,
|
primaryButtonDisabled,
|
||||||
}),
|
}),
|
||||||
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps): void =>
|
showOkModalDialog: (title: string, subText: string): void =>
|
||||||
get().openDialog({
|
get().openDialog({
|
||||||
isModal: true,
|
isModal: true,
|
||||||
title,
|
title,
|
||||||
@@ -94,7 +94,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
get().closeDialog();
|
get().closeDialog();
|
||||||
},
|
},
|
||||||
onSecondaryButtonClick: undefined,
|
onSecondaryButtonClick: undefined,
|
||||||
linkProps,
|
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogBody,
|
|
||||||
DialogContent,
|
|
||||||
DialogSurface,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
Field,
|
|
||||||
ProgressBar,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
interface ProgressModalDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
maxValue: number;
|
|
||||||
value: number;
|
|
||||||
dismissText: string;
|
|
||||||
onDismiss: () => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
/* mode drives the state of the action buttons
|
|
||||||
* inProgress: Show cancel button
|
|
||||||
* completed: Show close button
|
|
||||||
* aborting: Show cancel button, but disabled
|
|
||||||
* aborted: Show close button
|
|
||||||
*/
|
|
||||||
mode?: "inProgress" | "completed" | "aborting" | "aborted";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React component that renders a modal dialog with a progress bar.
|
|
||||||
*/
|
|
||||||
export const ProgressModalDialog: React.FC<ProgressModalDialogProps> = ({
|
|
||||||
isOpen,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
maxValue,
|
|
||||||
value,
|
|
||||||
dismissText,
|
|
||||||
onCancel,
|
|
||||||
onDismiss,
|
|
||||||
children,
|
|
||||||
mode = "completed",
|
|
||||||
}) => (
|
|
||||||
<Dialog
|
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={(event, data) => {
|
|
||||||
if (!data.open) {
|
|
||||||
onDismiss();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogSurface>
|
|
||||||
<DialogBody>
|
|
||||||
<DialogTitle>{title}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Field validationMessage={message} validationState="none">
|
|
||||||
<ProgressBar max={maxValue} value={value} />
|
|
||||||
</Field>
|
|
||||||
{children}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
{mode === "inProgress" || mode === "aborting" ? (
|
|
||||||
<Button appearance="secondary" onClick={onCancel} disabled={mode === "aborting"}>
|
|
||||||
{dismissText}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<DialogTrigger disableButtonEnhancement>
|
|
||||||
<Button appearance="primary">Close</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
)}
|
|
||||||
</DialogActions>
|
|
||||||
</DialogBody>
|
|
||||||
</DialogSurface>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
@@ -134,6 +134,7 @@ describe("SettingsComponent", () => {
|
|||||||
readSettings: undefined,
|
readSettings: undefined,
|
||||||
onSettingsClick: undefined,
|
onSettingsClick: undefined,
|
||||||
loadOffer: undefined,
|
loadOffer: undefined,
|
||||||
|
getPendingThroughputSplitNotification: undefined,
|
||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
newCollection.getDatabase = () => newDatabase;
|
newCollection.getDatabase = () => newDatabase;
|
||||||
newCollection.offer = ko.observable(undefined);
|
newCollection.offer = ko.observable(undefined);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
ContainerVectorPolicyComponent,
|
ContainerVectorPolicyComponent,
|
||||||
ContainerVectorPolicyComponentProps,
|
ContainerVectorPolicyComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
@@ -33,6 +32,7 @@ import {
|
|||||||
PartitionKeyComponent,
|
PartitionKeyComponent,
|
||||||
PartitionKeyComponentProps,
|
PartitionKeyComponentProps,
|
||||||
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
||||||
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
@@ -130,6 +130,7 @@ export interface SettingsComponentState {
|
|||||||
conflictResolutionPolicyProcedureBaseline: string;
|
conflictResolutionPolicyProcedureBaseline: string;
|
||||||
isConflictResolutionDirty: boolean;
|
isConflictResolutionDirty: boolean;
|
||||||
|
|
||||||
|
initialNotification: DataModels.Notification;
|
||||||
selectedTab: SettingsV2TabTypes;
|
selectedTab: SettingsV2TabTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +229,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
conflictResolutionPolicyProcedureBaseline: undefined,
|
conflictResolutionPolicyProcedureBaseline: undefined,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
|
|
||||||
|
initialNotification: undefined,
|
||||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1050,6 +1052,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
|
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
|
||||||
onScaleSaveableChange: this.onScaleSaveableChange,
|
onScaleSaveableChange: this.onScaleSaveableChange,
|
||||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||||
|
initialNotification: this.props.settingsTab.pendingNotification(),
|
||||||
throughputError: this.state.throughputError,
|
throughputError: this.state.throughputError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import ko from "knockout";
|
||||||
|
import React from "react";
|
||||||
import * as Constants from "../../../../Common/Constants";
|
import * as Constants from "../../../../Common/Constants";
|
||||||
|
import * as DataModels from "../../../../Contracts/DataModels";
|
||||||
import { updateUserContext } from "../../../../UserContext";
|
import { updateUserContext } from "../../../../UserContext";
|
||||||
import Explorer from "../../../Explorer";
|
import Explorer from "../../../Explorer";
|
||||||
|
import { throughputUnit } from "../SettingsRenderUtils";
|
||||||
import { collection } from "../TestUtils";
|
import { collection } from "../TestUtils";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||||
|
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||||
|
|
||||||
describe("ScaleComponent", () => {
|
describe("ScaleComponent", () => {
|
||||||
|
const targetThroughput = 6000;
|
||||||
|
|
||||||
const baseProps: ScaleComponentProps = {
|
const baseProps: ScaleComponentProps = {
|
||||||
collection: collection,
|
collection: collection,
|
||||||
database: undefined,
|
database: undefined,
|
||||||
@@ -28,8 +36,39 @@ describe("ScaleComponent", () => {
|
|||||||
onScaleDiscardableChange: () => {
|
onScaleDiscardableChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
initialNotification: {
|
||||||
|
description: `Throughput update for ${targetThroughput} ${throughputUnit}`,
|
||||||
|
} as DataModels.Notification,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
it("renders with correct initial notification", () => {
|
||||||
|
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||||
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
|
||||||
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
||||||
|
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(`${targetThroughput}`);
|
||||||
|
|
||||||
|
const newCollection = { ...collection };
|
||||||
|
const maxThroughput = 5000;
|
||||||
|
newCollection.offer = ko.observable({
|
||||||
|
manualThroughput: undefined,
|
||||||
|
autoscaleMaxThroughput: maxThroughput,
|
||||||
|
minimumThroughput: 400,
|
||||||
|
id: "offer",
|
||||||
|
offerReplacePending: true,
|
||||||
|
});
|
||||||
|
const newProps = {
|
||||||
|
...baseProps,
|
||||||
|
initialNotification: undefined as DataModels.Notification,
|
||||||
|
collection: newCollection,
|
||||||
|
};
|
||||||
|
wrapper = shallow(<ScaleComponent {...newProps} />);
|
||||||
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||||
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||||
|
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(`${maxThroughput}`);
|
||||||
|
});
|
||||||
|
|
||||||
it("autoScale disabled", () => {
|
it("autoScale disabled", () => {
|
||||||
const scaleComponent = new ScaleComponent(baseProps);
|
const scaleComponent = new ScaleComponent(baseProps);
|
||||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
|
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
|||||||
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
|
||||||
import {
|
import {
|
||||||
getTextFieldStyles,
|
getTextFieldStyles,
|
||||||
|
getThroughputApplyLongDelayMessage,
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
subComponentStackProps,
|
subComponentStackProps,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
@@ -33,6 +34,7 @@ export interface ScaleComponentProps {
|
|||||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
|
initialNotification: DataModels.Notification;
|
||||||
throughputError?: string;
|
throughputError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +102,10 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public getInitialNotificationElement = (): JSX.Element => {
|
public getInitialNotificationElement = (): JSX.Element => {
|
||||||
|
if (this.props.initialNotification) {
|
||||||
|
return this.getLongDelayMessage();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.offer?.offerReplacePending) {
|
if (this.offer?.offerReplacePending) {
|
||||||
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
|
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
@@ -114,6 +120,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getLongDelayMessage = (): JSX.Element => {
|
||||||
|
const matches: string[] = this.props.initialNotification?.description.match(
|
||||||
|
`Throughput update for (.*) ${throughputUnit}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const throughput = this.props.throughputBaseline;
|
||||||
|
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
|
||||||
|
if (targetThroughput) {
|
||||||
|
return getThroughputApplyLongDelayMessage(
|
||||||
|
this.props.wasAutopilotOriginallySet,
|
||||||
|
throughput,
|
||||||
|
throughputUnit,
|
||||||
|
this.databaseId,
|
||||||
|
this.collectionId,
|
||||||
|
targetThroughput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
private getThroughputInputComponent = (): JSX.Element => (
|
private getThroughputInputComponent = (): JSX.Element => (
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
databaseAccount={userContext?.databaseAccount}
|
databaseAccount={userContext?.databaseAccount}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledMessageBar
|
||||||
|
messageBarType={5}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
id="throughputApplyLongDelayMessage"
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"root": {
|
||||||
|
"color": "windowtext",
|
||||||
|
"fontSize": 14,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
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 />
|
||||||
|
Database: test, Container: test
|
||||||
|
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
|
||||||
|
</Text>
|
||||||
|
</StyledMessageBar>
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ThroughputInputAutoPilotV3Component
|
||||||
|
canExceedMaximumValue={true}
|
||||||
|
collectionName="test"
|
||||||
|
databaseName="test"
|
||||||
|
isAutoPilotSelected={false}
|
||||||
|
isEmulator={false}
|
||||||
|
isEnabled={true}
|
||||||
|
isFixed={false}
|
||||||
|
label="Throughput (6,000 - unlimited RU/s)"
|
||||||
|
maxAutoPilotThroughput={4000}
|
||||||
|
maxAutoPilotThroughputBaseline={4000}
|
||||||
|
maximum={1000000}
|
||||||
|
minimum={6000}
|
||||||
|
onAutoPilotSelected={[Function]}
|
||||||
|
onMaxAutoPilotThroughputChange={[Function]}
|
||||||
|
onScaleDiscardableChange={[Function]}
|
||||||
|
onScaleSaveableChange={[Function]}
|
||||||
|
onThroughputChange={[Function]}
|
||||||
|
spendAckChecked={false}
|
||||||
|
throughput={1000}
|
||||||
|
throughputBaseline={1000}
|
||||||
|
usageSizeInKB={100}
|
||||||
|
wasAutopilotOriginallySet={true}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
@@ -44,6 +44,7 @@ describe("SettingsUtils", () => {
|
|||||||
readSettings: undefined,
|
readSettings: undefined,
|
||||||
onSettingsClick: undefined,
|
onSettingsClick: undefined,
|
||||||
loadOffer: undefined,
|
loadOffer: undefined,
|
||||||
|
getPendingThroughputSplitNotification: undefined,
|
||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
};
|
};
|
||||||
newCollection.offer(undefined);
|
newCollection.offer(undefined);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const useTreeStyles = makeStyles({
|
|||||||
minWidth: "100%",
|
minWidth: "100%",
|
||||||
rowGap: "0px",
|
rowGap: "0px",
|
||||||
paddingTop: "0px",
|
paddingTop: "0px",
|
||||||
[treeIconWidth]: "16px",
|
[treeIconWidth]: "20px",
|
||||||
[leafNodeSpacing]: "24px",
|
[leafNodeSpacing]: "24px",
|
||||||
},
|
},
|
||||||
nodeIcon: {
|
nodeIcon: {
|
||||||
@@ -25,13 +25,12 @@ export const useTreeStyles = makeStyles({
|
|||||||
height: `var(${treeIconWidth})`,
|
height: `var(${treeIconWidth})`,
|
||||||
},
|
},
|
||||||
treeItem: {},
|
treeItem: {},
|
||||||
nodeLabel: {
|
nodeLabel: {},
|
||||||
whiteSpace: "nowrap", // Don't wrap text, there will be a scrollbar.
|
|
||||||
},
|
|
||||||
treeItemLayout: {
|
treeItemLayout: {
|
||||||
fontSize: tokens.fontSizeBase300,
|
fontSize: tokens.fontSizeBase300,
|
||||||
height: tokens.layoutRowHeight,
|
height: tokens.layoutRowHeight,
|
||||||
...cosmosShorthands.borderBottom(),
|
...cosmosShorthands.borderBottom(),
|
||||||
|
paddingLeft: `calc(var(${treeItemLevelToken}, 1) * ${tokens.spacingHorizontalXXL})`,
|
||||||
|
|
||||||
// Some sneaky CSS variables stuff to change the background color of the action button on hover.
|
// Some sneaky CSS variables stuff to change the background color of the action button on hover.
|
||||||
[actionButtonBackground]: tokens.colorNeutralBackground1,
|
[actionButtonBackground]: tokens.colorNeutralBackground1,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
|
|
||||||
const onOpenChange = useCallback(
|
const onOpenChange = useCallback(
|
||||||
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
||||||
if (data.type === "Click" && node.onClick) {
|
if (data.type === "Click" && !isBranch && node.onClick) {
|
||||||
node.onClick();
|
node.onClick();
|
||||||
}
|
}
|
||||||
if (!node.isExpanded && data.open && node.onExpanded) {
|
if (!node.isExpanded && data.open && node.onExpanded) {
|
||||||
@@ -119,7 +119,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
node.onCollapsed?.();
|
node.onCollapsed?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[node, setIsLoading],
|
[isBranch, node, setIsLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMenuOpenChange = useCallback(
|
const onMenuOpenChange = useCallback(
|
||||||
@@ -149,16 +149,15 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
|
|
||||||
// We use the expandIcon slot to hold the node icon too.
|
// We use the expandIcon slot to hold the node icon too.
|
||||||
// We only show a node icon for leaf nodes, even if a branch node has an iconSrc.
|
// We only show a node icon for leaf nodes, even if a branch node has an iconSrc.
|
||||||
const treeIcon =
|
const expandIcon = isLoading ? (
|
||||||
node.iconSrc === undefined ? undefined : typeof node.iconSrc === "string" ? (
|
<Spinner size="extra-tiny" />
|
||||||
|
) : !isBranch ? (
|
||||||
|
typeof node.iconSrc === "string" ? (
|
||||||
<img src={node.iconSrc} className={treeStyles.nodeIcon} alt="" />
|
<img src={node.iconSrc} className={treeStyles.nodeIcon} alt="" />
|
||||||
) : (
|
) : (
|
||||||
node.iconSrc
|
node.iconSrc
|
||||||
);
|
)
|
||||||
|
) : openItems.includes(treeNodeId) ? (
|
||||||
const expandIcon = isLoading ? (
|
|
||||||
<Spinner size="extra-tiny" />
|
|
||||||
) : !isBranch ? undefined : openItems.includes(treeNodeId) ? (
|
|
||||||
<ChevronDown20Regular data-test="TreeNode/CollapseIcon" />
|
<ChevronDown20Regular data-test="TreeNode/CollapseIcon" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight20Regular data-text="TreeNode/ExpandIcon" />
|
<ChevronRight20Regular data-text="TreeNode/ExpandIcon" />
|
||||||
@@ -175,6 +174,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
className={mergeClasses(
|
className={mergeClasses(
|
||||||
treeStyles.treeItemLayout,
|
treeStyles.treeItemLayout,
|
||||||
|
expandIcon ? undefined : treeStyles.treeItemLayoutNoIcon,
|
||||||
shouldShowAsSelected && treeStyles.selectedItem,
|
shouldShowAsSelected && treeStyles.selectedItem,
|
||||||
node.className && treeStyles[node.className],
|
node.className && treeStyles[node.className],
|
||||||
)}
|
)}
|
||||||
@@ -200,7 +200,6 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iconBefore={treeIcon}
|
|
||||||
expandIcon={expandIcon}
|
expandIcon={expandIcon}
|
||||||
>
|
>
|
||||||
<span className={treeStyles.nodeLabel}>{node.label}</span>
|
<span className={treeStyles.nodeLabel}>{node.label}</span>
|
||||||
|
|||||||
@@ -10,23 +10,16 @@ exports[`TreeNodeComponent does not render children if the node is loading 1`] =
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconBefore={
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -163,7 +156,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -186,21 +179,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -225,7 +208,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -248,28 +231,18 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
class="fui-Tree rnv2ez3 ___jy13a00_lpffjy0 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
role="tree"
|
role="tree"
|
||||||
>
|
>
|
||||||
@@ -283,7 +256,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -306,21 +279,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child1Icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child1Label
|
child1Label
|
||||||
</span>
|
</span>
|
||||||
@@ -337,7 +300,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -360,21 +323,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child2LoadingIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child2LoadingLabel
|
child2LoadingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -390,7 +343,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -410,21 +363,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child3ExpandingIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child3ExpandingLabel
|
child3ExpandingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -440,23 +383,16 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconBefore={
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -483,21 +419,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</ChevronRight20Regular>
|
</ChevronRight20Regular>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden={true}
|
|
||||||
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout__main rklbe47"
|
className="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -505,7 +431,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeProvider
|
<TreeProvider
|
||||||
@@ -573,7 +499,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="fui-Tree rnv2ez3 ___jy13a00_lpffjy0 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
role="tree"
|
role="tree"
|
||||||
>
|
>
|
||||||
@@ -661,7 +587,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -684,21 +610,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child1Icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child1Label
|
child1Label
|
||||||
</span>
|
</span>
|
||||||
@@ -723,7 +639,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -746,21 +662,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child1Icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child1Label
|
child1Label
|
||||||
</span>
|
</span>
|
||||||
@@ -774,23 +680,16 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconBefore={
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child1Icon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -817,21 +716,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</ChevronRight20Regular>
|
</ChevronRight20Regular>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden={true}
|
|
||||||
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child1Icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout__main rklbe47"
|
className="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
child1Label
|
child1Label
|
||||||
</span>
|
</span>
|
||||||
@@ -839,7 +728,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root/child1Label"
|
data-test="Tree:root/child1Label"
|
||||||
>
|
>
|
||||||
<TreeProvider
|
<TreeProvider
|
||||||
@@ -932,7 +821,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -955,21 +844,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child2LoadingIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child2LoadingLabel
|
child2LoadingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -994,7 +873,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1017,21 +896,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child2LoadingIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child2LoadingLabel
|
child2LoadingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1045,23 +914,16 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconBefore={
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child2LoadingIcon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1088,21 +950,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</ChevronRight20Regular>
|
</ChevronRight20Regular>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden={true}
|
|
||||||
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child2LoadingIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout__main rklbe47"
|
className="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
child2LoadingLabel
|
child2LoadingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1187,7 +1039,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "leaf",
|
"itemType": "leaf",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1207,21 +1059,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child3ExpandingIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child3ExpandingLabel
|
child3ExpandingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1245,7 +1087,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1265,21 +1107,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="child3ExpandingIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="___1h29e9h_0000000 fz5stix"
|
class=""
|
||||||
>
|
>
|
||||||
child3ExpandingLabel
|
child3ExpandingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1293,9 +1125,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1304,12 +1136,12 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
className="fui-TreeItemLayout__expandIcon rh4pu5o"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
@@ -1321,7 +1153,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
className="fui-TreeItemLayout__main rklbe47"
|
className="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
child3ExpandingLabel
|
child3ExpandingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1352,9 +1184,9 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1363,7 +1195,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1381,23 +1213,16 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<Spinner
|
<Spinner
|
||||||
size="extra-tiny"
|
size="extra-tiny"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconBefore={
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1415,23 +1240,16 @@ exports[`TreeNodeComponent renders a node as expandable if it has empty, but def
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconBefore={
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1495,9 +1313,9 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
"className": "___1r8p62d_0000000 f1xg1ack f1e31b4d",
|
"className": "___1r8p62d_0000000 f1xg1ack f1e31b4d",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1506,7 +1324,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1545,9 +1363,9 @@ exports[`TreeNodeComponent renders a single node 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1556,7 +1374,7 @@ exports[`TreeNodeComponent renders a single node 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1574,9 +1392,9 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1585,7 +1403,7 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1603,29 +1421,22 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
className="___kqkdor0_ihxn0o0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconBefore={
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeNodeComponent
|
<TreeNodeComponent
|
||||||
@@ -1686,29 +1497,22 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
iconBefore={
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
|
||||||
src="rootIcon"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeNodeComponent
|
<TreeNodeComponent
|
||||||
@@ -1770,9 +1574,9 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
className="___kqkdor0_ihxn0o0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1781,7 +1585,7 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="___1h29e9h_0000000 fz5stix"
|
className=""
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
import * as msal from "@azure/msal-browser";
|
||||||
import { Link } from "@fluentui/react/lib/Link";
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
@@ -48,6 +46,7 @@ import { useTabs } from "../hooks/useTabs";
|
|||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||||
@@ -1120,7 +1119,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openUploadItemsPane(): void {
|
public openUploadItemsPanePane(): void {
|
||||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
||||||
}
|
}
|
||||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||||
@@ -1179,11 +1178,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async configureCopilot(): Promise<void> {
|
public async configureCopilot(): Promise<void> {
|
||||||
if (
|
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
|
||||||
userContext.apiType !== "SQL" ||
|
|
||||||
!userContext.subscriptionId ||
|
|
||||||
![Environment.Development, Environment.Mpac, Environment.Prod].includes(getEnvironment())
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const copilotEnabledPromise = getCopilotEnabled();
|
const copilotEnabledPromise = getCopilotEnabled();
|
||||||
|
|||||||
@@ -4,51 +4,83 @@
|
|||||||
* and update any knockout observables passed from the parent.
|
* and update any knockout observables passed from the parent.
|
||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import {
|
|
||||||
createPlatformButtons,
|
|
||||||
createStaticCommandBarButtons,
|
|
||||||
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import { Platform, configContext } from "../../../ConfigContext";
|
import { Platform, configContext } from "../../../ConfigContext";
|
||||||
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
|
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommandBarStore {
|
||||||
|
contextButtons: CommandButtonComponentProps[];
|
||||||
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
||||||
|
isHidden: boolean;
|
||||||
|
setIsHidden: (isHidden: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||||
|
contextButtons: [],
|
||||||
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||||
|
isHidden: false,
|
||||||
|
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||||
|
}));
|
||||||
|
|
||||||
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
const selectedNodeState = useSelectedNode();
|
const selectedNodeState = useSelectedNode();
|
||||||
const contextButtons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
|
|
||||||
const platformButtons = createPlatformButtons();
|
|
||||||
|
|
||||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor, container);
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
if (contextButtons?.length > 0) {
|
const buttons =
|
||||||
|
userContext.apiType === "Postgres"
|
||||||
|
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
||||||
|
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
||||||
|
return (
|
||||||
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
|
<FluentCommandBar
|
||||||
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
|
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
|
||||||
|
styles={{
|
||||||
|
root: { backgroundColor: backgroundColor },
|
||||||
|
}}
|
||||||
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
||||||
|
const contextButtons = (buttons || []).concat(
|
||||||
|
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
||||||
|
);
|
||||||
|
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
|
||||||
|
|
||||||
|
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
||||||
|
if (buttons && buttons.length > 0) {
|
||||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(
|
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
||||||
contextButtons || [],
|
|
||||||
backgroundColor,
|
|
||||||
container,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (uiFabricTabsButtons.length > 0) {
|
if (uiFabricTabsButtons.length > 0) {
|
||||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricPlatformButtons = CommandBarUtil.convertButton(platformButtons || [], backgroundColor, container);
|
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||||
uiFabricPlatformButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
|
|
||||||
@@ -56,7 +88,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
||||||
connectionInfo?.status !== ConnectionStatusType.Connect
|
connectionInfo?.status !== ConnectionStatusType.Connect
|
||||||
) {
|
) {
|
||||||
uiFabricPlatformButtons.unshift(
|
uiFabricControlButtons.unshift(
|
||||||
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -75,8 +107,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons, container);
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||||
setKeyboardHandlers(keyboardHandlers);
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,7 +116,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||||
farItems={uiFabricPlatformButtons}
|
farItems={uiFabricControlButtons}
|
||||||
styles={rootStyle}
|
styles={rootStyle}
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { AuthType } from "../../../AuthType";
|
|||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import { updateUserContext } from "../../../UserContext";
|
import { updateUserContext } from "../../../UserContext";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||||
|
|
||||||
describe("CommandBarComponentButtonFactory tests", () => {
|
describe("CommandBarComponentButtonFactory tests", () => {
|
||||||
|
let mockExplorer: Explorer;
|
||||||
|
|
||||||
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
||||||
|
|
||||||
describe("Enable Azure Synapse Link Button", () => {
|
describe("Enable Azure Synapse Link Button", () => {
|
||||||
@@ -16,6 +19,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -26,7 +30,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Button should be visible", () => {
|
it("Button should be visible", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -42,7 +46,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -58,7 +62,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -71,6 +75,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -103,7 +108,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
},
|
},
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -113,13 +118,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
portalEnv: "mooncake",
|
portalEnv: "mooncake",
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -129,8 +134,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const openPostgresShellButtonLabel = "Open PSQL shell";
|
const openPostgresShellButtonLabel = "Open PSQL shell";
|
||||||
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
|
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
|
});
|
||||||
|
|
||||||
it("creates Postgres shell button", () => {
|
it("creates Postgres shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createPostgreButtons();
|
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer);
|
||||||
const openPostgresShellButton = buttons.find(
|
const openPostgresShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -138,7 +147,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates vCore Mongo shell button", () => {
|
it("creates vCore Mongo shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons();
|
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
|
||||||
const openVCoreMongoShellButton = buttons.find(
|
const openVCoreMongoShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -153,6 +162,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.ResourceToken,
|
authType: AuthType.ResourceToken,
|
||||||
});
|
});
|
||||||
@@ -164,7 +175,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
kind: "DocumentDB",
|
kind: "DocumentDB",
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
expect(buttons.length).toBe(2);
|
expect(buttons.length).toBe(2);
|
||||||
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
||||||
expect(buttons[0].disabled).toBe(false);
|
expect(buttons[0].disabled).toBe(false);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { userContext } from "../../../UserContext";
|
|||||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
@@ -31,20 +32,19 @@ import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
|
|||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
|
export function createStaticCommandBarButtons(
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
container: Explorer,
|
||||||
return userContext.apiType === "Postgres" ? createPostgreButtons() : createVCoreMongoButtons();
|
selectedNodeState: SelectedNodeState,
|
||||||
}
|
): CommandButtonComponentProps[] {
|
||||||
|
|
||||||
if (userContext.authType === AuthType.ResourceToken) {
|
if (userContext.authType === AuthType.ResourceToken) {
|
||||||
return createStaticCommandBarButtonsForResourceToken(selectedNodeState);
|
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
// Avoid starting with a divider
|
// Avoid starting with a divider
|
||||||
const addDivider = () => {
|
const addDivider = () => {
|
||||||
if (buttons.length > 0 && !buttons[buttons.length - 1].isDivider) {
|
if (buttons.length > 0) {
|
||||||
buttons.push(createDivider());
|
buttons.push(createDivider());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables" &&
|
||||||
userContext.apiType !== "Cassandra"
|
userContext.apiType !== "Cassandra"
|
||||||
) {
|
) {
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton();
|
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||||
if (addSynapseLink) {
|
if (addSynapseLink) {
|
||||||
addDivider();
|
addDivider();
|
||||||
buttons.push(addSynapseLink);
|
buttons.push(addSynapseLink);
|
||||||
@@ -67,9 +67,9 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buttonProps = createLoginForEntraIDButton();
|
const buttonProps = createLoginForEntraIDButton(container);
|
||||||
setLoginButtonProps(buttonProps);
|
setLoginButtonProps(buttonProps);
|
||||||
}, [dataPlaneRbacEnabled, aadTokenUpdated]);
|
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
||||||
|
|
||||||
if (loginButtonProps) {
|
if (loginButtonProps) {
|
||||||
addDivider();
|
addDivider();
|
||||||
@@ -87,8 +87,8 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
||||||
const openQueryBtn = createOpenQueryButton();
|
const openQueryBtn = createOpenQueryButton(container);
|
||||||
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||||
buttons.push(openQueryBtn);
|
buttons.push(openQueryBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,6 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
tooltipText: userContext.features.commandBarV2 ? "New..." : label,
|
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -116,12 +115,21 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createContextCommandBarButtons(
|
||||||
|
container: Explorer,
|
||||||
|
selectedNodeState: SelectedNodeState,
|
||||||
|
): CommandButtonComponentProps[] {
|
||||||
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||||
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
||||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled) {
|
if (useNotebook.getState().isShellEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
@@ -133,7 +141,6 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
addDivider();
|
|
||||||
buttons.push(newMongoShellBtn);
|
buttons.push(newMongoShellBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,26 +153,28 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
const newCassandraShellButton: CommandButtonComponentProps = {
|
const newCassandraShellButton: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
addDivider();
|
|
||||||
buttons.push(newCassandraShellButton);
|
buttons.push(newCassandraShellButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPlatformButtons(): CommandButtonComponentProps[] {
|
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [
|
const buttons: CommandButtonComponentProps[] =
|
||||||
|
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
||||||
|
? []
|
||||||
|
: [
|
||||||
{
|
{
|
||||||
iconSrc: SettingsIcon,
|
iconSrc: SettingsIcon,
|
||||||
iconAlt: "Settings",
|
iconAlt: "Settings",
|
||||||
onCommandClick: (_, container) =>
|
onCommandClick: () =>
|
||||||
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: "Settings",
|
ariaLabel: "Settings",
|
||||||
@@ -202,7 +211,7 @@ export function createPlatformButtons(): CommandButtonComponentProps[] {
|
|||||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||||
iconSrc: FeedbackIcon,
|
iconSrc: FeedbackIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => container.openCESCVAFeedbackBlade(),
|
onCommandClick: () => container.openCESCVAFeedbackBlade(),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
tooltipText: label,
|
tooltipText: label,
|
||||||
@@ -234,7 +243,7 @@ function areScriptsSupported(): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -252,7 +261,7 @@ function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
|||||||
return {
|
return {
|
||||||
iconSrc: SynapseIcon,
|
iconSrc: SynapseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => container.openEnableSynapseLinkDialog(),
|
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -261,12 +270,12 @@ function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoginForEntraIDButton(): CommandButtonComponentProps {
|
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (configContext.platform !== Platform.Portal) {
|
if (configContext.platform !== Platform.Portal) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCommandClick: CommandButtonComponentProps["onCommandClick"] = async (_, container) => {
|
const handleCommandClick = async () => {
|
||||||
await container.openLoginForEntraIDPopUp();
|
await container.openLoginForEntraIDPopUp();
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
};
|
};
|
||||||
@@ -393,14 +402,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
|||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenQueryButton(): CommandButtonComponentProps {
|
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "Open Query";
|
const label = "Open Query";
|
||||||
return {
|
return {
|
||||||
iconSrc: BrowseQueriesIcon,
|
iconSrc: BrowseQueriesIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
tooltipText: userContext.features.commandBarV2 ? "Open Query..." : "Open Query",
|
|
||||||
keyboardAction: KeyboardAction.OPEN_QUERY,
|
keyboardAction: KeyboardAction.OPEN_QUERY,
|
||||||
onCommandClick: (_, container) =>
|
onCommandClick: () =>
|
||||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@@ -423,7 +431,10 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind): CommandButtonComponentProps {
|
function createOpenTerminalButtonByKind(
|
||||||
|
container: Explorer,
|
||||||
|
terminalKind: ViewModels.TerminalKind,
|
||||||
|
): CommandButtonComponentProps {
|
||||||
const terminalFriendlyName = (): string => {
|
const terminalFriendlyName = (): string => {
|
||||||
switch (terminalKind) {
|
switch (terminalKind) {
|
||||||
case ViewModels.TerminalKind.Cassandra:
|
case ViewModels.TerminalKind.Cassandra:
|
||||||
@@ -446,7 +457,7 @@ function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind):
|
|||||||
return {
|
return {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(terminalKind);
|
container.openNotebookTerminal(terminalKind);
|
||||||
}
|
}
|
||||||
@@ -460,10 +471,11 @@ function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createStaticCommandBarButtonsForResourceToken(
|
function createStaticCommandBarButtonsForResourceToken(
|
||||||
|
container: Explorer,
|
||||||
selectedNodeState: SelectedNodeState,
|
selectedNodeState: SelectedNodeState,
|
||||||
): CommandButtonComponentProps[] {
|
): CommandButtonComponentProps[] {
|
||||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||||
const openQueryBtn = createOpenQueryButton();
|
const openQueryBtn = createOpenQueryButton(container);
|
||||||
|
|
||||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||||
const isResourceTokenCollectionNodeSelected: boolean =
|
const isResourceTokenCollectionNodeSelected: boolean =
|
||||||
@@ -476,20 +488,20 @@ function createStaticCommandBarButtonsForResourceToken(
|
|||||||
|
|
||||||
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||||
if (!openQueryBtn.disabled) {
|
if (!openQueryBtn.disabled) {
|
||||||
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [newSqlQueryBtn, openQueryBtn];
|
return [newSqlQueryBtn, openQueryBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPostgreButtons(): CommandButtonComponentProps[] {
|
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const openPostgreShellBtn = createOpenTerminalButtonByKind(ViewModels.TerminalKind.Postgres);
|
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
|
||||||
|
|
||||||
return [openPostgreShellBtn];
|
return [openPostgreShellBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVCoreMongoButtons(): CommandButtonComponentProps[] {
|
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(ViewModels.TerminalKind.VCoreMongo);
|
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
|
||||||
|
|
||||||
return [openVCoreMongoTerminalButton];
|
return [openVCoreMongoTerminalButton];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { ICommandBarItemProps } from "@fluentui/react";
|
import { ICommandBarItemProps } from "@fluentui/react";
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
|
||||||
describe("CommandBarUtil tests", () => {
|
describe("CommandBarUtil tests", () => {
|
||||||
const mockExplorer = {} as Explorer;
|
|
||||||
const createButton = (): CommandButtonComponentProps => {
|
const createButton = (): CommandButtonComponentProps => {
|
||||||
return {
|
return {
|
||||||
iconSrc: "icon",
|
iconSrc: "icon",
|
||||||
@@ -24,7 +22,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const btn = createButton();
|
const btn = createButton();
|
||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer);
|
const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
|
||||||
expect(converteds.length).toBe(1);
|
expect(converteds.length).toBe(1);
|
||||||
const converted = converteds[0];
|
const converted = converteds[0];
|
||||||
expect(converted.split).toBe(undefined);
|
expect(converted.split).toBe(undefined);
|
||||||
@@ -48,7 +46,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btn.children.push(child);
|
btn.children.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor", mockExplorer);
|
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
|
||||||
expect(converteds.length).toBe(1);
|
expect(converteds.length).toBe(1);
|
||||||
const converted = converteds[0];
|
const converted = converteds[0];
|
||||||
expect(converted.split).toBe(true);
|
expect(converted.split).toBe(true);
|
||||||
@@ -64,7 +62,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btns.push(createButton());
|
btns.push(createButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor", mockExplorer);
|
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
|
||||||
const uniqueKeys = converteds
|
const uniqueKeys = converteds
|
||||||
.map((btn: ICommandBarItemProps) => btn.key)
|
.map((btn: ICommandBarItemProps) => btn.key)
|
||||||
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
||||||
@@ -76,10 +74,10 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
btn.commandButtonLabel = undefined;
|
btn.commandButtonLabel = undefined;
|
||||||
let converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
|
|
||||||
converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
||||||
delete btn.commandButtonLabel;
|
delete btn.commandButtonLabel;
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
|||||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||||
* @param btns
|
* @param btns
|
||||||
*/
|
*/
|
||||||
export const convertButton = (
|
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||||
btns: CommandButtonComponentProps[],
|
|
||||||
backgroundColor: string,
|
|
||||||
container: Explorer,
|
|
||||||
): ICommandBarItemProps[] => {
|
|
||||||
const buttonHeightPx =
|
const buttonHeightPx =
|
||||||
configContext.platform == Platform.Fabric
|
configContext.platform == Platform.Fabric
|
||||||
? StyleConstants.FabricCommandBarButtonHeight
|
? StyleConstants.FabricCommandBarButtonHeight
|
||||||
@@ -58,14 +54,15 @@ export const convertButton = (
|
|||||||
iconProps: {
|
iconProps: {
|
||||||
style: {
|
style: {
|
||||||
width: StyleConstants.CommandBarIconWidth, // 16
|
width: StyleConstants.CommandBarIconWidth, // 16
|
||||||
alignSelf: undefined,
|
alignSelf: btn.iconName ? "baseline" : undefined,
|
||||||
filter: getFilter(btn.disabled),
|
filter: getFilter(btn.disabled),
|
||||||
},
|
},
|
||||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||||
|
iconName: btn.iconName,
|
||||||
},
|
},
|
||||||
onClick: btn.onCommandClick
|
onClick: btn.onCommandClick
|
||||||
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||||
btn.onCommandClick(ev, container);
|
btn.onCommandClick(ev);
|
||||||
let copilotEnabled = false;
|
let copilotEnabled = false;
|
||||||
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
||||||
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
||||||
@@ -138,7 +135,7 @@ export const convertButton = (
|
|||||||
result.split = true;
|
result.split = true;
|
||||||
|
|
||||||
result.subMenuProps = {
|
result.subMenuProps = {
|
||||||
items: convertButton(btn.children, backgroundColor, container),
|
items: convertButton(btn.children, backgroundColor),
|
||||||
styles: {
|
styles: {
|
||||||
list: {
|
list: {
|
||||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||||
@@ -189,7 +186,7 @@ export const convertButton = (
|
|||||||
option?: IDropdownOption,
|
option?: IDropdownOption,
|
||||||
index?: number,
|
index?: number,
|
||||||
): void => {
|
): void => {
|
||||||
btn.children[index].onCommandClick(event, container);
|
btn.children[index].onCommandClick(event);
|
||||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,17 +237,14 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createKeyboardHandlers(
|
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
||||||
allButtons: CommandButtonComponentProps[],
|
|
||||||
container: Explorer,
|
|
||||||
): KeyboardHandlerMap {
|
|
||||||
const handlers: KeyboardHandlerMap = {};
|
const handlers: KeyboardHandlerMap = {};
|
||||||
|
|
||||||
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
if (!button.disabled && button.keyboardAction) {
|
if (!button.disabled && button.keyboardAction) {
|
||||||
handlers[button.keyboardAction] = (e) => {
|
handlers[button.keyboardAction] = (e) => {
|
||||||
button.onCommandClick(e, container);
|
button.onCommandClick(e);
|
||||||
|
|
||||||
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import create, { UseStore } from "zustand";
|
|
||||||
|
|
||||||
export interface CommandBarStore {
|
|
||||||
contextButtons: CommandButtonComponentProps[];
|
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
|
||||||
isHidden: boolean;
|
|
||||||
setIsHidden: (isHidden: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
|
||||||
contextButtons: [],
|
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
|
||||||
isHidden: false,
|
|
||||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
|
||||||
}));
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import {
|
|
||||||
makeStyles,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
MenuPopover,
|
|
||||||
MenuTrigger,
|
|
||||||
Toolbar,
|
|
||||||
ToolbarButton,
|
|
||||||
ToolbarDivider,
|
|
||||||
ToolbarGroup,
|
|
||||||
Tooltip,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
createPlatformButtons,
|
|
||||||
createStaticCommandBarButtons,
|
|
||||||
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
|
|
||||||
import { createKeyboardHandlers } from "Explorer/Menus/CommandBar/CommandBarUtil";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
|
||||||
import React, { MouseEventHandler } from "react";
|
|
||||||
|
|
||||||
const useToolbarStyles = makeStyles({
|
|
||||||
toolbar: {
|
|
||||||
height: tokens.layoutRowHeight,
|
|
||||||
justifyContent: "space-between", // Ensures that the two toolbar groups are at opposite ends of the toolbar
|
|
||||||
...cosmosShorthands.borderBottom(),
|
|
||||||
},
|
|
||||||
toolbarGroup: {
|
|
||||||
display: "flex",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface CommandBarV2Props {
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandBarV2: React.FC<CommandBarV2Props> = ({ explorer }: CommandBarV2Props) => {
|
|
||||||
const styles = useToolbarStyles();
|
|
||||||
const selectedNodeState = useSelectedNode();
|
|
||||||
const contextButtons = useCommandBar((state) => state.contextButtons);
|
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
|
||||||
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
|
|
||||||
const platformButtons = createPlatformButtons();
|
|
||||||
|
|
||||||
if (isHidden) {
|
|
||||||
setKeyboardHandlers({});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
|
||||||
const keyboardHandlers = createKeyboardHandlers(allButtons, explorer);
|
|
||||||
setKeyboardHandlers(keyboardHandlers);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CosmosFluentProvider>
|
|
||||||
<Toolbar className={styles.toolbar}>
|
|
||||||
<ToolbarGroup role="presentation" className={styles.toolbarGroup}>
|
|
||||||
{staticButtons.map((button, index) =>
|
|
||||||
renderButton(explorer, button, `static-${index}`, contextButtons?.length > 0),
|
|
||||||
)}
|
|
||||||
{staticButtons.length > 0 && contextButtons?.length > 0 && <ToolbarDivider />}
|
|
||||||
{contextButtons.map((button, index) => renderButton(explorer, button, `context-${index}`, false))}
|
|
||||||
</ToolbarGroup>
|
|
||||||
<ToolbarGroup role="presentation">
|
|
||||||
{platformButtons.map((button, index) => renderButton(explorer, button, `platform-${index}`, true))}
|
|
||||||
</ToolbarGroup>
|
|
||||||
</Toolbar>
|
|
||||||
</CosmosFluentProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// This allows us to migrate individual buttons over to using a JSX.Element for the icon, without requiring us to change them all at once.
|
|
||||||
function renderIcon(iconSrcOrElement: string | JSX.Element, alt?: string): JSX.Element {
|
|
||||||
if (typeof iconSrcOrElement === "string") {
|
|
||||||
return <img src={iconSrcOrElement} alt={alt} />;
|
|
||||||
}
|
|
||||||
return iconSrcOrElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderButton(
|
|
||||||
explorer: Explorer,
|
|
||||||
btn: CommandButtonComponentProps,
|
|
||||||
key: string,
|
|
||||||
iconOnly: boolean,
|
|
||||||
): JSX.Element {
|
|
||||||
if (btn.isDivider) {
|
|
||||||
return <ToolbarDivider key={key} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChildren = !!btn.children && btn.children.length > 0;
|
|
||||||
const label = btn.commandButtonLabel || btn.tooltipText;
|
|
||||||
const tooltip = btn.tooltipText || (iconOnly ? label : undefined);
|
|
||||||
const onClick: MouseEventHandler | undefined =
|
|
||||||
btn.onCommandClick && !hasChildren ? (e) => btn.onCommandClick(e, explorer) : undefined;
|
|
||||||
|
|
||||||
// We don't know which element will be the top-level element, so just slap a key on all of 'em
|
|
||||||
|
|
||||||
let button = hasChildren ? (
|
|
||||||
<MenuButton key={key} appearance="subtle" aria-label={btn.ariaLabel} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{!iconOnly && label}
|
|
||||||
</MenuButton>
|
|
||||||
) : (
|
|
||||||
<ToolbarButton key={key} aria-label={btn.ariaLabel} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{!iconOnly && label}
|
|
||||||
</ToolbarButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tooltip) {
|
|
||||||
button = (
|
|
||||||
<Tooltip key={key} content={tooltip} relationship="description" withArrow>
|
|
||||||
{button}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChildren) {
|
|
||||||
button = (
|
|
||||||
<Menu key={key}>
|
|
||||||
<MenuTrigger disableButtonEnhancement>{button}</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMenuItem(explorer: Explorer, btn: CommandButtonComponentProps, key: string): JSX.Element {
|
|
||||||
const hasChildren = !!btn.children && btn.children.length > 0;
|
|
||||||
const onClick: MouseEventHandler | undefined = btn.onCommandClick
|
|
||||||
? (e) => btn.onCommandClick(e, explorer)
|
|
||||||
: undefined;
|
|
||||||
const item = (
|
|
||||||
<MenuItem key={key} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{btn.commandButtonLabel || btn.tooltipText}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasChildren) {
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<MenuTrigger disableButtonEnhancement>{item}</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { clear, collectionWasOpened, getItems, Type } from "Explorer/MostRecentActivity/MostRecentActivity";
|
|
||||||
import { observable } from "knockout";
|
import { observable } from "knockout";
|
||||||
|
import { mostRecentActivity } from "./MostRecentActivity";
|
||||||
|
|
||||||
describe("MostRecentActivity", () => {
|
describe("MostRecentActivity", () => {
|
||||||
const accountName = "some account";
|
const accountId = "some account";
|
||||||
|
|
||||||
beforeEach(() => clear(accountName));
|
beforeEach(() => mostRecentActivity.clear(accountId));
|
||||||
|
|
||||||
it("Has no items at first", () => {
|
it("Has no items at first", () => {
|
||||||
expect(getItems(accountName)).toStrictEqual([]);
|
expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can record collections being opened", () => {
|
it("Can record collections being opened", () => {
|
||||||
@@ -18,9 +18,9 @@ describe("MostRecentActivity", () => {
|
|||||||
databaseId,
|
databaseId,
|
||||||
};
|
};
|
||||||
|
|
||||||
collectionWasOpened(accountName, collection);
|
mostRecentActivity.collectionWasOpened(accountId, collection);
|
||||||
|
|
||||||
const activity = getItems(accountName);
|
const activity = mostRecentActivity.getItems(accountId);
|
||||||
expect(activity).toEqual([
|
expect(activity).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
collectionId,
|
collectionId,
|
||||||
@@ -29,24 +29,58 @@ describe("MostRecentActivity", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Does not store duplicate entries", () => {
|
it("Can record notebooks being opened", () => {
|
||||||
const collectionId = "some collection";
|
const name = "some notebook";
|
||||||
const databaseId = "some database";
|
const path = "some path";
|
||||||
const collection = {
|
const notebook = { name, path };
|
||||||
id: observable(collectionId),
|
|
||||||
databaseId,
|
|
||||||
};
|
|
||||||
|
|
||||||
collectionWasOpened(accountName, collection);
|
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
||||||
collectionWasOpened(accountName, collection);
|
|
||||||
|
|
||||||
const activity = getItems(accountName);
|
const activity = mostRecentActivity.getItems(accountId);
|
||||||
expect(activity).toEqual([
|
expect(activity).toEqual([expect.objectContaining(notebook)]);
|
||||||
expect.objectContaining({
|
});
|
||||||
type: Type.OpenCollection,
|
|
||||||
collectionId,
|
it("Filters out duplicates", () => {
|
||||||
databaseId,
|
const name = "some notebook";
|
||||||
}),
|
const path = "some path";
|
||||||
]);
|
const notebook = { name, path };
|
||||||
|
const sameNotebook = { name, path };
|
||||||
|
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook);
|
||||||
|
|
||||||
|
const activity = mostRecentActivity.getItems(accountId);
|
||||||
|
expect(activity.length).toEqual(1);
|
||||||
|
expect(activity).toEqual([expect.objectContaining(notebook)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows for multiple accounts", () => {
|
||||||
|
const name = "some notebook";
|
||||||
|
const path = "some path";
|
||||||
|
const notebook = { name, path };
|
||||||
|
|
||||||
|
const anotherNotebook = { name: "Another " + name, path };
|
||||||
|
const anotherAccountId = "Another " + accountId;
|
||||||
|
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
||||||
|
mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook);
|
||||||
|
|
||||||
|
expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]);
|
||||||
|
expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Can store multiple distinct elements, in FIFO order", () => {
|
||||||
|
const name = "some notebook";
|
||||||
|
const path = "some path";
|
||||||
|
const first = { name, path };
|
||||||
|
const second = { name: "Another " + name, path };
|
||||||
|
const third = { name, path: "Another " + path };
|
||||||
|
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, first);
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, second);
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, third);
|
||||||
|
|
||||||
|
const activity = mostRecentActivity.getItems(accountId);
|
||||||
|
expect(activity).toEqual([third, second, first].map(expect.objectContaining));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { AppStateComponentNames, deleteState, loadState, saveState } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
|
||||||
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
OpenCollection = "OpenCollection",
|
OpenCollection,
|
||||||
OpenNotebook = "OpenNotebook",
|
OpenNotebook,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenNotebookItem {
|
export interface OpenNotebookItem {
|
||||||
@@ -21,174 +21,158 @@ export interface OpenCollectionItem {
|
|||||||
|
|
||||||
type Item = OpenNotebookItem | OpenCollectionItem;
|
type Item = OpenNotebookItem | OpenCollectionItem;
|
||||||
|
|
||||||
const itemsMaxNumber: number = 5;
|
// Update schemaVersion if you are going to change this interface
|
||||||
|
interface StoredData {
|
||||||
|
schemaVersion: string;
|
||||||
|
itemsMap: { [accountId: string]: Item[] }; // FIFO
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate old data to new AppState
|
* Stores most recent activity
|
||||||
*/
|
*/
|
||||||
const migrateOldData = () => {
|
class MostRecentActivity {
|
||||||
|
private static readonly schemaVersion: string = "2";
|
||||||
|
private static itemsMaxNumber: number = 5;
|
||||||
|
private storedData: StoredData;
|
||||||
|
constructor() {
|
||||||
|
// Retrieve from local storage
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
||||||
const oldDataSchemaVersion: string = "2";
|
|
||||||
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
||||||
if (rawData) {
|
|
||||||
const oldData = JSON.parse(rawData);
|
if (!rawData) {
|
||||||
if (oldData.schemaVersion === oldDataSchemaVersion) {
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
const itemsMap: Record<string, Item[]> = oldData.itemsMap;
|
} else {
|
||||||
Object.keys(itemsMap).forEach((accountId: string) => {
|
try {
|
||||||
const accountName = accountId.split("/").pop();
|
this.storedData = JSON.parse(rawData);
|
||||||
if (accountName) {
|
} catch (e) {
|
||||||
saveState(
|
console.error("Unable to parse stored most recent activity. Use empty data:", rawData);
|
||||||
{
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
},
|
|
||||||
itemsMap[accountId].map((item) => {
|
|
||||||
if ((item.type as unknown as number) === 0) {
|
|
||||||
item.type = Type.OpenCollection;
|
|
||||||
} else if ((item.type as unknown as number) === 1) {
|
|
||||||
item.type = Type.OpenNotebook;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old data
|
// If version doesn't match or schema broke, nuke it!
|
||||||
|
if (
|
||||||
|
!this.storedData.hasOwnProperty("schemaVersion") ||
|
||||||
|
this.storedData["schemaVersion"] !== MostRecentActivity.schemaVersion
|
||||||
|
) {
|
||||||
|
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
||||||
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let p in this.storedData.itemsMap) {
|
||||||
|
this.cleanupItems(p);
|
||||||
|
}
|
||||||
|
this.saveToLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createEmptyData(): StoredData {
|
||||||
|
return {
|
||||||
|
schemaVersion: MostRecentActivity.schemaVersion,
|
||||||
|
itemsMap: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isEmpty(object: any) {
|
||||||
|
return Object.keys(object).length === 0 && object.constructor === Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveToLocalStorage() {
|
||||||
|
if (MostRecentActivity.isEmpty(this.storedData.itemsMap)) {
|
||||||
|
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
||||||
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
||||||
}
|
}
|
||||||
};
|
// Don't save if empty
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const addItem = (accountName: string, newItem: Item): void => {
|
LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData));
|
||||||
|
}
|
||||||
|
|
||||||
|
private addItem(accountId: string, newItem: Item): void {
|
||||||
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
|
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
|
||||||
// if (!accountId) {
|
// if (!accountId) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
let items =
|
|
||||||
(loadState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
}) as Item[]) || [];
|
|
||||||
|
|
||||||
// Remove duplicate
|
// Remove duplicate
|
||||||
items = removeDuplicate(newItem, items);
|
MostRecentActivity.removeDuplicate(newItem, this.storedData.itemsMap[accountId]);
|
||||||
|
|
||||||
items.unshift(newItem);
|
this.storedData.itemsMap[accountId] = this.storedData.itemsMap[accountId] || [];
|
||||||
items = cleanupItems(items, accountName);
|
this.storedData.itemsMap[accountId].unshift(newItem);
|
||||||
saveState(
|
this.cleanupItems(accountId);
|
||||||
{
|
this.saveToLocalStorage();
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
},
|
|
||||||
items,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getItems = (accountName: string): Item[] => {
|
|
||||||
if (!accountName) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
public getItems(accountId: string): Item[] {
|
||||||
(loadState({
|
return this.storedData.itemsMap[accountId] || [];
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
}) as Item[]) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const collectionWasOpened = (
|
|
||||||
accountName: string,
|
|
||||||
{ id, databaseId }: Pick<CollectionBase, "id" | "databaseId">,
|
|
||||||
) => {
|
|
||||||
if (accountName === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
|
||||||
const collectionId = id();
|
const collectionId = id();
|
||||||
addItem(accountName, {
|
this.addItem(accountId, {
|
||||||
type: Type.OpenCollection,
|
type: Type.OpenCollection,
|
||||||
databaseId,
|
databaseId,
|
||||||
collectionId,
|
collectionId,
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
export const clear = (accountName: string): void => {
|
public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
|
||||||
if (!accountName) {
|
this.addItem(accountId, {
|
||||||
|
type: Type.OpenNotebook,
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(accountId: string): void {
|
||||||
|
delete this.storedData.itemsMap[accountId];
|
||||||
|
this.saveToLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find items by doing strict comparison and remove from array if duplicate is found
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
private static removeDuplicate(item: Item, itemsArray: Item[]): void {
|
||||||
|
if (!itemsArray) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sort object by key
|
|
||||||
const sortObjectKeys = (unordered: Record<string, unknown>): Record<string, unknown> => {
|
|
||||||
return Object.keys(unordered)
|
|
||||||
.sort()
|
|
||||||
.reduce((obj: Record<string, unknown>, key: string) => {
|
|
||||||
obj[key] = unordered[key];
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find items by doing strict comparison and remove from array if duplicate is found.
|
|
||||||
* Modifies the array.
|
|
||||||
* @param item
|
|
||||||
* @param itemsArray
|
|
||||||
* @returns new array
|
|
||||||
*/
|
|
||||||
const removeDuplicate = (item: Item, itemsArray: Item[]): Item[] => {
|
|
||||||
if (!itemsArray) {
|
|
||||||
return itemsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: Item[] = [...itemsArray];
|
|
||||||
|
|
||||||
let index = -1;
|
let index = -1;
|
||||||
for (let i = 0; i < result.length; i++) {
|
for (let i = 0; i < itemsArray.length; i++) {
|
||||||
const currentItem = result[i];
|
const currentItem = itemsArray[i];
|
||||||
|
if (JSON.stringify(currentItem) === JSON.stringify(item)) {
|
||||||
if (
|
|
||||||
JSON.stringify(sortObjectKeys(currentItem as unknown as Record<string, unknown>)) ===
|
|
||||||
JSON.stringify(sortObjectKeys(item as unknown as Record<string, unknown>))
|
|
||||||
) {
|
|
||||||
index = i;
|
index = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
result.splice(index, 1);
|
itemsArray.splice(index, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove unknown types
|
* Remove unknown types
|
||||||
* Limit items to max number
|
* Limit items to max number
|
||||||
* Modifies the array.
|
|
||||||
*/
|
*/
|
||||||
const cleanupItems = (items: Item[], accountName: string): Item[] => {
|
private cleanupItems(accountId: string): void {
|
||||||
if (accountName === undefined) {
|
if (!this.storedData.itemsMap.hasOwnProperty(accountId)) {
|
||||||
return [];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsArray = items.filter((item) => item.type in Type).slice(0, itemsMaxNumber);
|
const itemsArray = this.storedData.itemsMap[accountId]
|
||||||
|
.filter((item) => item.type in Type)
|
||||||
|
.slice(0, MostRecentActivity.itemsMaxNumber);
|
||||||
if (itemsArray.length === 0) {
|
if (itemsArray.length === 0) {
|
||||||
deleteState({
|
delete this.storedData.itemsMap[accountId];
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
} else {
|
||||||
globalAccountName: accountName,
|
this.storedData.itemsMap[accountId] = itemsArray;
|
||||||
});
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return itemsArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
migrateOldData();
|
export const mostRecentActivity = new MostRecentActivity();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
@@ -128,9 +127,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||||
? databaseAccount?.location
|
? databaseAccount?.location
|
||||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||||
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations)
|
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||||
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
|
|
||||||
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(disallowedLocationsUri, {
|
const response = await fetch(disallowedLocationsUri, {
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ChoiceGroup,
|
ChoiceGroup,
|
||||||
DefaultButton,
|
|
||||||
IChoiceGroupOption,
|
IChoiceGroupOption,
|
||||||
ISpinButtonStyles,
|
ISpinButtonStyles,
|
||||||
IToggleStyles,
|
IToggleStyles,
|
||||||
|
Icon,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarType,
|
MessageBarType,
|
||||||
Position,
|
Position,
|
||||||
SpinButton,
|
SpinButton,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
TooltipHost,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, makeStyles } from "@fluentui/react-components";
|
|
||||||
import { AuthType } from "AuthType";
|
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { SplitterDirection } from "Common/Splitter";
|
import { SplitterDirection } from "Common/Splitter";
|
||||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { deleteAllStates } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import {
|
import {
|
||||||
DefaultRUThreshold,
|
DefaultRUThreshold,
|
||||||
LocalStorageUtility,
|
LocalStorageUtility,
|
||||||
@@ -32,13 +29,14 @@ import * as StringUtility from "Shared/StringUtility";
|
|||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import create, { UseStore } from "zustand";
|
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
import { AuthType } from "AuthType";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
|
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
|
|
||||||
export interface DataPlaneRbacState {
|
export interface DataPlaneRbacState {
|
||||||
dataPlaneRbacEnabled: boolean;
|
dataPlaneRbacEnabled: boolean;
|
||||||
@@ -52,39 +50,6 @@ export interface DataPlaneRbacState {
|
|||||||
|
|
||||||
type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>;
|
type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>;
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
bulletList: {
|
|
||||||
listStyleType: "disc",
|
|
||||||
paddingLeft: "20px",
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
firstItem: {
|
|
||||||
flex: "1",
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
marginRight: "5px",
|
|
||||||
},
|
|
||||||
headerIcon: {
|
|
||||||
paddingTop: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
settingsSectionContainer: {
|
|
||||||
paddingLeft: "15px",
|
|
||||||
},
|
|
||||||
settingsSectionDescription: {
|
|
||||||
paddingBottom: "10px",
|
|
||||||
fontSize: "12px",
|
|
||||||
},
|
|
||||||
subHeader: {
|
|
||||||
marginRight: "5px",
|
|
||||||
fontSize: "12px",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
|
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
|
||||||
dataPlaneRbacEnabled: false,
|
dataPlaneRbacEnabled: false,
|
||||||
}));
|
}));
|
||||||
@@ -168,9 +133,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>(
|
const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>(
|
||||||
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
|
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
const explorerVersion = configContext.gitSha;
|
const explorerVersion = configContext.gitSha;
|
||||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
||||||
@@ -191,7 +153,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
|
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Fabric) {
|
|
||||||
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
||||||
if (
|
if (
|
||||||
enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption ||
|
enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption ||
|
||||||
@@ -231,7 +192,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: false });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
||||||
@@ -468,19 +428,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className={`paneMainContent ${styles.container}`}>
|
<div className="paneMainContent">
|
||||||
<Accordion className={styles.firstItem}>
|
|
||||||
{shouldShowQueryPageOptions && (
|
{shouldShowQueryPageOptions && (
|
||||||
<AccordionItem value="1">
|
<div className="settingsSection">
|
||||||
<AccordionHeader>
|
<div className="settingsSectionPart">
|
||||||
<div className={styles.header}>Page Options</div>
|
<fieldset>
|
||||||
</AccordionHeader>
|
<legend id="pageOptions" className="settingsSectionLabel legendLabel">
|
||||||
<AccordionPanel>
|
Page Options
|
||||||
<div className={styles.settingsSectionContainer}>
|
</legend>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<InfoTooltip>
|
||||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
|
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
|
||||||
many query results per page.
|
query results per page.
|
||||||
</div>
|
</InfoTooltip>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="pageOptions"
|
ariaLabelledBy="pageOptions"
|
||||||
selectedKey={pageOption}
|
selectedKey={pageOption}
|
||||||
@@ -488,15 +447,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={choiceButtonStyles}
|
styles={choiceButtonStyles}
|
||||||
onChange={handleOnPageOptionChange}
|
onChange={handleOnPageOptionChange}
|
||||||
/>
|
/>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div className={`tabs ${styles.settingsSectionContainer}`}>
|
<div className="tabs settingsSectionPart">
|
||||||
{isCustomPageOptionSelected() && (
|
{isCustomPageOptionSelected() && (
|
||||||
<div className="tabcontent">
|
<div className="tabcontent">
|
||||||
<div className={styles.settingsSectionDescription}>
|
<div className="settingsSectionLabel">
|
||||||
Query results per page{" "}
|
Query results per page
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
<InfoTooltip>Enter the number of query results that should be shown per page.</InfoTooltip>
|
||||||
Enter the number of query results that should be shown per page.
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SpinButton
|
<SpinButton
|
||||||
@@ -516,18 +474,34 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "SQL" &&
|
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && (
|
||||||
userContext.authType === AuthType.AAD &&
|
<>
|
||||||
configContext.platform !== Platform.Fabric && (
|
<div className="settingsSection">
|
||||||
<AccordionItem value="2">
|
<div className="settingsSectionPart">
|
||||||
<AccordionHeader>
|
<fieldset>
|
||||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
<legend id="enableDataPlaneRBACOptions" className="settingsSectionLabel legendLabel">
|
||||||
</AccordionHeader>
|
Enable Entra ID RBAC
|
||||||
<AccordionPanel>
|
</legend>
|
||||||
<div className={styles.settingsSectionContainer}>
|
<TooltipHost
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||||
|
ID RBAC.
|
||||||
|
<a
|
||||||
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
Learn more{" "}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon iconName="Info" ariaLabel="Info tooltip" className="panelInfoIcon" tabIndex={0} />
|
||||||
|
</TooltipHost>
|
||||||
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
@@ -539,18 +513,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
operations
|
operations
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
|
||||||
ID RBAC.
|
|
||||||
<a
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
Learn more{" "}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||||
options={dataPlaneRBACOptionsList}
|
options={dataPlaneRBACOptionsList}
|
||||||
@@ -558,23 +520,58 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
selectedKey={enableDataPlaneRBACOption}
|
selectedKey={enableDataPlaneRBACOption}
|
||||||
onChange={handleOnDataPlaneRBACOptionChange}
|
onChange={handleOnDataPlaneRBACOptionChange}
|
||||||
/>
|
/>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<>
|
<>
|
||||||
<AccordionItem value="3">
|
<div className="settingsSection">
|
||||||
<AccordionHeader>
|
<div className="settingsSectionPart">
|
||||||
<div className={styles.header}>Query Timeout</div>
|
<div>
|
||||||
</AccordionHeader>
|
<legend id="ruThresholdLabel" className="settingsSectionLabel legendLabel">
|
||||||
<AccordionPanel>
|
RU Threshold
|
||||||
<div className={styles.settingsSectionContainer}>
|
</legend>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<InfoTooltip>If a query exceeds a configured RU threshold, the query will be aborted.</InfoTooltip>
|
||||||
When a query reaches a specified time limit, a popup with an option to cancel the query will show
|
|
||||||
unless automatic cancellation has been enabled.
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
styles={toggleStyles}
|
||||||
|
label="Enable RU threshold"
|
||||||
|
onChange={handleOnRUThresholdToggleChange}
|
||||||
|
defaultChecked={ruThresholdEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{ruThresholdEnabled && (
|
||||||
|
<div>
|
||||||
|
<SpinButton
|
||||||
|
label="RU Threshold (RU)"
|
||||||
|
labelPosition={Position.top}
|
||||||
|
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
||||||
|
min={1}
|
||||||
|
step={1000}
|
||||||
|
onChange={handleOnRUThresholdSpinButtonChange}
|
||||||
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
|
styles={spinButtonStyles}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
|
<div>
|
||||||
|
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
|
||||||
|
Query Timeout
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>
|
||||||
|
When a query reaches a specified time limit, a popup with an option to cancel the query will show
|
||||||
|
unless automatic cancellation has been enabled
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<Toggle
|
<Toggle
|
||||||
styles={toggleStyles}
|
styles={toggleStyles}
|
||||||
label="Enable query timeout"
|
label="Enable query timeout"
|
||||||
@@ -583,7 +580,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{queryTimeoutEnabled && (
|
{queryTimeoutEnabled && (
|
||||||
<div className={styles.settingsSectionContainer}>
|
<div>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
label="Query timeout (ms)"
|
label="Query timeout (ms)"
|
||||||
labelPosition={Position.top}
|
labelPosition={Position.top}
|
||||||
@@ -603,52 +600,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="4">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>RU Limit</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
If a query exceeds a configured RU limit, the query will be aborted.
|
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
|
||||||
styles={toggleStyles}
|
|
||||||
label="Enable RU limit"
|
|
||||||
onChange={handleOnRUThresholdToggleChange}
|
|
||||||
defaultChecked={ruThresholdEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{ruThresholdEnabled && (
|
<div className="settingsSection">
|
||||||
<div className={styles.settingsSectionContainer}>
|
<div className="settingsSectionPart">
|
||||||
<SpinButton
|
<div>
|
||||||
label="RU Limit (RU)"
|
<legend id="defaultQueryResultsView" className="settingsSectionLabel legendLabel">
|
||||||
labelPosition={Position.top}
|
Default Query Results View
|
||||||
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
</legend>
|
||||||
min={1}
|
<InfoTooltip>Select the default view to use when displaying query results.</InfoTooltip>
|
||||||
step={1000}
|
|
||||||
onChange={handleOnRUThresholdSpinButtonChange}
|
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
|
||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
|
||||||
styles={spinButtonStyles}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="5">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Default Query Results View</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Select the default view to use when displaying query results.
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="defaultQueryResultsView"
|
ariaLabelledBy="defaultQueryResultsView"
|
||||||
selectedKey={defaultQueryResultsView}
|
selectedKey={defaultQueryResultsView}
|
||||||
@@ -657,25 +619,21 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
onChange={handleOnDefaultQueryResultsViewChange}
|
onChange={handleOnDefaultQueryResultsViewChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<div className="settingsSection">
|
||||||
<AccordionItem value="6">
|
<div className="settingsSectionPart">
|
||||||
<AccordionHeader>
|
<div className="settingsSectionLabel">
|
||||||
<div className={styles.header}>Retry Settings</div>
|
Retry Settings
|
||||||
</AccordionHeader>
|
<InfoTooltip>Retry policy associated with throttled requests during CosmosDB queries.</InfoTooltip>
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Retry policy associated with throttled requests during CosmosDB queries.
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className={styles.subHeader}>Max retry attempts</span>
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
Max retry attempts
|
||||||
Max number of retries to be performed for a request. Default value 9.
|
</legend>
|
||||||
</InfoTooltip>
|
<InfoTooltip>Max number of retries to be performed for a request. Default value 9.</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
labelPosition={Position.top}
|
labelPosition={Position.top}
|
||||||
@@ -691,10 +649,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={spinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
Fixed retry interval (ms)
|
||||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
|
</legend>
|
||||||
part of the response. Default value is 0 milliseconds.
|
<InfoTooltip>
|
||||||
|
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part
|
||||||
|
of the response. Default value is 0 milliseconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
@@ -711,8 +671,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={spinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
Max wait time (s)
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>
|
||||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||||
seconds.
|
seconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
@@ -731,17 +693,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={spinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
||||||
<AccordionItem value="7">
|
<div className="settingsSectionLabel">
|
||||||
<AccordionHeader>
|
Enable container pagination
|
||||||
<div className={styles.header}>Enable container pagination</div>
|
<InfoTooltip>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
styles={{
|
styles={{
|
||||||
@@ -751,23 +710,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
ariaLabel="Enable container pagination"
|
ariaLabel="Enable container pagination"
|
||||||
checked={containerPaginationEnabled}
|
checked={containerPaginationEnabled}
|
||||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||||
label="Enable container pagination"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
{shouldShowCrossPartitionOption && (
|
|
||||||
<AccordionItem value="8">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Enable cross-partition query</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Send more than one request while executing a query. More than one request is necessary if the query
|
|
||||||
is not scoped to single partition key value.
|
|
||||||
</div>
|
</div>
|
||||||
|
{shouldShowCrossPartitionOption && (
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Enable cross-partition query
|
||||||
|
<InfoTooltip>
|
||||||
|
Send more than one request while executing a query. More than one request is necessary if the query is
|
||||||
|
not scoped to single partition key value.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
styles={{
|
styles={{
|
||||||
label: { padding: 0 },
|
label: { padding: 0 },
|
||||||
@@ -776,25 +732,22 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
ariaLabel="Enable cross partition query"
|
ariaLabel="Enable cross partition query"
|
||||||
checked={crossPartitionQueryEnabled}
|
checked={crossPartitionQueryEnabled}
|
||||||
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
|
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
|
||||||
label="Enable cross-partition query"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowParallelismOption && (
|
{shouldShowParallelismOption && (
|
||||||
<AccordionItem value="9">
|
<div className="settingsSection">
|
||||||
<AccordionHeader>
|
<div className="settingsSectionPart">
|
||||||
<div className={styles.header}>Max degree of parallelism</div>
|
<div className="settingsSectionLabel">
|
||||||
</AccordionHeader>
|
Max degree of parallelism
|
||||||
<AccordionPanel>
|
<InfoTooltip>
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
||||||
positive property value limits the number of concurrent operations to the set value. If it is set to
|
positive property value limits the number of concurrent operations to the set value. If it is set to
|
||||||
less than 0, the system automatically decides the number of concurrent operations to run.
|
less than 0, the system automatically decides the number of concurrent operations to run.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SpinButton
|
<SpinButton
|
||||||
min={-1}
|
min={-1}
|
||||||
step={1}
|
step={1}
|
||||||
@@ -802,33 +755,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
role="textbox"
|
role="textbox"
|
||||||
id="max-degree"
|
id="max-degree"
|
||||||
value={"" + maxDegreeOfParallelism}
|
value={"" + maxDegreeOfParallelism}
|
||||||
onIncrement={(newValue) =>
|
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
||||||
setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)
|
onDecrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)}
|
||||||
}
|
|
||||||
onDecrement={(newValue) =>
|
|
||||||
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
|
|
||||||
}
|
|
||||||
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
|
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
|
||||||
ariaLabel="Max degree of parallelism"
|
ariaLabel="Max degree of parallelism"
|
||||||
label="Max degree of parallelism"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowPriorityLevelOption && (
|
{shouldShowPriorityLevelOption && (
|
||||||
<AccordionItem value="10">
|
<div className="settingsSection">
|
||||||
<AccordionHeader>
|
<div className="settingsSectionPart">
|
||||||
<div className={styles.header}>Priority Level</div>
|
<fieldset>
|
||||||
</AccordionHeader>
|
<legend id="priorityLevel" className="settingsSectionLabel legendLabel">
|
||||||
<AccordionPanel>
|
Priority Level
|
||||||
<div className={styles.settingsSectionContainer}>
|
</legend>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<InfoTooltip>
|
||||||
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
|
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
|
||||||
Execution. If "None" is selected, Data Explorer will not specify priority level, and the
|
Execution. If "None" is selected, Data Explorer will not specify priority level, and the
|
||||||
server-side default priority level will be used.
|
server-side default priority level will be used.
|
||||||
</div>
|
</InfoTooltip>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="priorityLevel"
|
ariaLabelledBy="priorityLevel"
|
||||||
selectedKey={priorityLevel}
|
selectedKey={priorityLevel}
|
||||||
@@ -836,22 +782,21 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={choiceButtonStyles}
|
styles={choiceButtonStyles}
|
||||||
onChange={handleOnPriorityLevelOptionChange}
|
onChange={handleOnPriorityLevelOptionChange}
|
||||||
/>
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowGraphAutoVizOption && (
|
{shouldShowGraphAutoVizOption && (
|
||||||
<AccordionItem value="11">
|
<div className="settingsSection">
|
||||||
<AccordionHeader>
|
<div className="settingsSectionPart">
|
||||||
<div className={styles.header}>Display Gremlin query results as: </div>
|
<div className="settingsSectionLabel">
|
||||||
</AccordionHeader>
|
Display Gremlin query results as:
|
||||||
<AccordionPanel>
|
<InfoTooltip>
|
||||||
<div className={styles.settingsSectionContainer}>
|
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as
|
||||||
<div className={styles.settingsSectionDescription}>
|
JSON.
|
||||||
Select Graph to automatically visualize the query results as a Graph or JSON to display the results
|
</InfoTooltip>
|
||||||
as JSON.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
selectedKey={graphAutoVizDisabled}
|
selectedKey={graphAutoVizDisabled}
|
||||||
options={graphAutoOptionList}
|
options={graphAutoOptionList}
|
||||||
@@ -859,22 +804,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
aria-label="Graph Auto-visualization"
|
aria-label="Graph Auto-visualization"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{shouldShowCopilotSampleDBOption && (
|
|
||||||
<AccordionItem value="12">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Enable sample database</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
This is a sample database and collection with synthetic product data you can use to explore using
|
|
||||||
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and
|
|
||||||
is created by, and maintained by Microsoft at no cost to you.
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{shouldShowCopilotSampleDBOption && (
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Enable sample database
|
||||||
|
<InfoTooltip>
|
||||||
|
This is a sample database and collection with synthetic product data you can use to explore using
|
||||||
|
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is
|
||||||
|
created by, and maintained by Microsoft at no cost to you.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
styles={{
|
styles={{
|
||||||
label: { padding: 0 },
|
label: { padding: 0 },
|
||||||
@@ -883,42 +826,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
ariaLabel="Enable sample db for Query Advisor"
|
ariaLabel="Enable sample db for Query Advisor"
|
||||||
checked={copilotSampleDBEnabled}
|
checked={copilotSampleDBEnabled}
|
||||||
onChange={handleSampleDatabaseChange}
|
onChange={handleSampleDatabaseChange}
|
||||||
label="Enable sample database"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
)}
|
)}
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<div className="settingsSection">
|
|
||||||
<div className="settingsSectionPart">
|
|
||||||
<DefaultButton
|
|
||||||
onClick={() => {
|
|
||||||
useDialog.getState().showOkCancelModalDialog(
|
|
||||||
"Clear History",
|
|
||||||
undefined,
|
|
||||||
"Are you sure you want to proceed?",
|
|
||||||
() => deleteAllStates(),
|
|
||||||
"Cancel",
|
|
||||||
undefined,
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
This action will clear the all customizations for this account in this browser, including:
|
|
||||||
</span>
|
|
||||||
<ul className={styles.bulletList}>
|
|
||||||
<li>Reset your customized tab layout, including the splitter positions</li>
|
|
||||||
<li>Erase your table column preferences, including any custom columns</li>
|
|
||||||
<li>Clear your filter history</li>
|
|
||||||
</ul>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear History
|
|
||||||
</DefaultButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSectionPart">
|
||||||
<div className="settingsSectionLabel">Explorer Version</div>
|
<div className="settingsSectionLabel">Explorer Version</div>
|
||||||
|
|||||||
@@ -8,30 +8,24 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
submitButtonText="Apply"
|
submitButtonText="Apply"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
className="paneMainContent"
|
||||||
>
|
>
|
||||||
<Accordion
|
|
||||||
className="___1uf6361_0000000 fz7g6wx"
|
|
||||||
>
|
|
||||||
<AccordionItem
|
|
||||||
value="1"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<fieldset>
|
||||||
|
<legend
|
||||||
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="pageOptions"
|
||||||
>
|
>
|
||||||
Page Options
|
Page Options
|
||||||
</div>
|
</legend>
|
||||||
</AccordionHeader>
|
<InfoTooltip>
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
||||||
</div>
|
</InfoTooltip>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
ariaLabelledBy="pageOptions"
|
ariaLabelledBy="pageOptions"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -72,21 +66,19 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="tabs ___1dfa554_0000000 fo7qwa0"
|
className="tabs settingsSectionPart"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="tabcontent"
|
className="tabcontent"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
className="settingsSectionLabel"
|
||||||
>
|
>
|
||||||
Query results per page
|
Query results per page
|
||||||
|
<InfoTooltip>
|
||||||
<InfoTooltip
|
|
||||||
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
|
||||||
>
|
|
||||||
Enter the number of query results that should be shown per page.
|
Enter the number of query results that should be shown per page.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,71 +96,28 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem
|
|
||||||
value="3"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
|
||||||
className="___15c001r_0000000 fq02s40"
|
|
||||||
>
|
|
||||||
Query Timeout
|
|
||||||
</div>
|
</div>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
<div
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
className="settingsSectionPart"
|
||||||
>
|
>
|
||||||
When a query reaches a specified time limit, a popup with an option to cancel the query will show unless automatic cancellation has been enabled.
|
<div>
|
||||||
</div>
|
<legend
|
||||||
<StyledToggleBase
|
className="settingsSectionLabel legendLabel"
|
||||||
defaultChecked={false}
|
id="ruThresholdLabel"
|
||||||
label="Enable query timeout"
|
>
|
||||||
onChange={[Function]}
|
RU Threshold
|
||||||
styles={
|
</legend>
|
||||||
{
|
<InfoTooltip>
|
||||||
"container": {},
|
If a query exceeds a configured RU threshold, the query will be aborted.
|
||||||
"label": {
|
</InfoTooltip>
|
||||||
"display": "block",
|
|
||||||
"fontSize": 12,
|
|
||||||
"fontWeight": 400,
|
|
||||||
},
|
|
||||||
"pill": {},
|
|
||||||
"root": {},
|
|
||||||
"text": {},
|
|
||||||
"thumb": {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem
|
|
||||||
value="4"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
|
||||||
className="___15c001r_0000000 fq02s40"
|
|
||||||
>
|
|
||||||
RU Limit
|
|
||||||
</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
If a query exceeds a configured RU limit, the query will be aborted.
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<StyledToggleBase
|
<StyledToggleBase
|
||||||
defaultChecked={true}
|
defaultChecked={true}
|
||||||
label="Enable RU limit"
|
label="Enable RU threshold"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -186,14 +135,12 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div>
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<StyledSpinButton
|
<StyledSpinButton
|
||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
defaultValue="5000"
|
defaultValue="5000"
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
label="RU Limit (RU)"
|
label="RU Threshold (RU)"
|
||||||
labelPosition={0}
|
labelPosition={0}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -216,27 +163,66 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
</div>
|
||||||
<AccordionItem
|
|
||||||
value="5"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<legend
|
||||||
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="queryTimeoutLabel"
|
||||||
|
>
|
||||||
|
Query Timeout
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>
|
||||||
|
When a query reaches a specified time limit, a popup with an option to cancel the query will show unless automatic cancellation has been enabled
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<StyledToggleBase
|
||||||
|
defaultChecked={false}
|
||||||
|
label="Enable query timeout"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"container": {},
|
||||||
|
"label": {
|
||||||
|
"display": "block",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 400,
|
||||||
|
},
|
||||||
|
"pill": {},
|
||||||
|
"root": {},
|
||||||
|
"text": {},
|
||||||
|
"thumb": {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<legend
|
||||||
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="defaultQueryResultsView"
|
||||||
>
|
>
|
||||||
Default Query Results View
|
Default Query Results View
|
||||||
</div>
|
</legend>
|
||||||
</AccordionHeader>
|
<InfoTooltip>
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Select the default view to use when displaying query results.
|
Select the default view to use when displaying query results.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
ariaLabelledBy="defaultQueryResultsView"
|
ariaLabelledBy="defaultQueryResultsView"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -252,7 +238,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
selectedKey="horizontal"
|
selectedKey="vertical"
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
"flexContainer": [
|
"flexContainer": [
|
||||||
@@ -278,36 +264,30 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
</div>
|
||||||
<AccordionItem
|
|
||||||
value="6"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
>
|
>
|
||||||
Retry Settings
|
Retry Settings
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Retry policy associated with throttled requests during CosmosDB queries.
|
Retry policy associated with throttled requests during CosmosDB queries.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<legend
|
||||||
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="queryRetryAttemptsLabel"
|
||||||
>
|
>
|
||||||
Max retry attempts
|
Max retry attempts
|
||||||
</span>
|
</legend>
|
||||||
<InfoTooltip
|
<InfoTooltip>
|
||||||
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
|
||||||
>
|
|
||||||
Max number of retries to be performed for a request. Default value 9.
|
Max number of retries to be performed for a request. Default value 9.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -340,14 +320,13 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
value="9"
|
value="9"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<legend
|
||||||
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="queryRetryAttemptsLabel"
|
||||||
>
|
>
|
||||||
Fixed retry interval (ms)
|
Fixed retry interval (ms)
|
||||||
</span>
|
</legend>
|
||||||
<InfoTooltip
|
<InfoTooltip>
|
||||||
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
|
||||||
>
|
|
||||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds.
|
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -380,14 +359,13 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<legend
|
||||||
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="queryRetryAttemptsLabel"
|
||||||
>
|
>
|
||||||
Max wait time (s)
|
Max wait time (s)
|
||||||
</span>
|
</legend>
|
||||||
<InfoTooltip
|
<InfoTooltip>
|
||||||
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
|
||||||
>
|
|
||||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
|
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -420,32 +398,25 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
value="30"
|
value="30"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem
|
|
||||||
value="7"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart settingsSectionInlineCheckbox"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
>
|
>
|
||||||
Enable container pagination
|
Enable container pagination
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
ariaLabel="Enable container pagination"
|
ariaLabel="Enable container pagination"
|
||||||
checked={false}
|
checked={false}
|
||||||
className="padding"
|
className="padding"
|
||||||
label="Enable container pagination"
|
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -456,32 +427,25 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem
|
|
||||||
value="8"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart settingsSectionInlineCheckbox"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
>
|
>
|
||||||
Enable cross-partition query
|
Enable cross-partition query
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
|
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
ariaLabel="Enable cross partition query"
|
ariaLabel="Enable cross partition query"
|
||||||
checked={false}
|
checked={false}
|
||||||
className="padding"
|
className="padding"
|
||||||
label="Enable cross-partition query"
|
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -492,32 +456,25 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem
|
|
||||||
value="9"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
>
|
>
|
||||||
Max degree of parallelism
|
Max degree of parallelism
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
|
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<StyledSpinButton
|
<StyledSpinButton
|
||||||
ariaLabel="Max degree of parallelism"
|
ariaLabel="Max degree of parallelism"
|
||||||
className="textfontclr"
|
className="textfontclr"
|
||||||
id="max-degree"
|
id="max-degree"
|
||||||
label="Max degree of parallelism"
|
|
||||||
min={-1}
|
min={-1}
|
||||||
onDecrement={[Function]}
|
onDecrement={[Function]}
|
||||||
onIncrement={[Function]}
|
onIncrement={[Function]}
|
||||||
@@ -527,21 +484,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
value="6"
|
value="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
<div
|
|
||||||
className="settingsSection"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionPart"
|
|
||||||
>
|
|
||||||
<CustomizedDefaultButton
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
Clear History
|
|
||||||
</CustomizedDefaultButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
@@ -569,39 +511,30 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
submitButtonText="Apply"
|
submitButtonText="Apply"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
className="paneMainContent"
|
||||||
>
|
>
|
||||||
<Accordion
|
|
||||||
className="___1uf6361_0000000 fz7g6wx"
|
|
||||||
>
|
|
||||||
<AccordionItem
|
|
||||||
value="6"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
>
|
>
|
||||||
Retry Settings
|
Retry Settings
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Retry policy associated with throttled requests during CosmosDB queries.
|
Retry policy associated with throttled requests during CosmosDB queries.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<legend
|
||||||
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="queryRetryAttemptsLabel"
|
||||||
>
|
>
|
||||||
Max retry attempts
|
Max retry attempts
|
||||||
</span>
|
</legend>
|
||||||
<InfoTooltip
|
<InfoTooltip>
|
||||||
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
|
||||||
>
|
|
||||||
Max number of retries to be performed for a request. Default value 9.
|
Max number of retries to be performed for a request. Default value 9.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -634,14 +567,13 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
value="9"
|
value="9"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<legend
|
||||||
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="queryRetryAttemptsLabel"
|
||||||
>
|
>
|
||||||
Fixed retry interval (ms)
|
Fixed retry interval (ms)
|
||||||
</span>
|
</legend>
|
||||||
<InfoTooltip
|
<InfoTooltip>
|
||||||
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
|
||||||
>
|
|
||||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds.
|
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -674,14 +606,13 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<legend
|
||||||
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
className="settingsSectionLabel legendLabel"
|
||||||
|
id="queryRetryAttemptsLabel"
|
||||||
>
|
>
|
||||||
Max wait time (s)
|
Max wait time (s)
|
||||||
</span>
|
</legend>
|
||||||
<InfoTooltip
|
<InfoTooltip>
|
||||||
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
|
||||||
>
|
|
||||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
|
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -714,32 +645,25 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
value="30"
|
value="30"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem
|
|
||||||
value="7"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart settingsSectionInlineCheckbox"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
>
|
>
|
||||||
Enable container pagination
|
Enable container pagination
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
ariaLabel="Enable container pagination"
|
ariaLabel="Enable container pagination"
|
||||||
checked={false}
|
checked={false}
|
||||||
className="padding"
|
className="padding"
|
||||||
label="Enable container pagination"
|
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -750,26 +674,20 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem
|
|
||||||
value="11"
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<div
|
<div
|
||||||
className="___15c001r_0000000 fq02s40"
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionPart"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="settingsSectionLabel"
|
||||||
>
|
>
|
||||||
Display Gremlin query results as:
|
Display Gremlin query results as:
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div
|
|
||||||
className="___1dfa554_0000000 fo7qwa0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
|
||||||
>
|
|
||||||
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.
|
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
aria-label="Graph Auto-visualization"
|
aria-label="Graph Auto-visualization"
|
||||||
@@ -789,21 +707,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
selectedKey="false"
|
selectedKey="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
<div
|
|
||||||
className="settingsSection"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionPart"
|
|
||||||
>
|
|
||||||
<CustomizedDefaultButton
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
Clear History
|
|
||||||
</CustomizedDefaultButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
CheckboxOnChangeData,
|
|
||||||
InputOnChangeData,
|
|
||||||
makeStyles,
|
|
||||||
SearchBox,
|
|
||||||
SearchBoxChangeEvent,
|
|
||||||
Text,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import { configContext } from "ConfigContext";
|
|
||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
|
||||||
import { CosmosFluentProvider, getPlatformTheme } from "Explorer/Theme/ThemeUtil";
|
|
||||||
import React from "react";
|
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
|
|
||||||
const useColumnSelectionStyles = makeStyles({
|
|
||||||
paneContainer: {
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
},
|
|
||||||
searchBox: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
checkboxContainer: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
checkboxLabel: {
|
|
||||||
padding: "4px 8px",
|
|
||||||
marginBottom: "0px",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export interface TableColumnSelectionPaneProps {
|
|
||||||
columnDefinitions: ColumnDefinition[];
|
|
||||||
selectedColumnIds: string[];
|
|
||||||
onSelectionChange: (newSelectedColumnIds: string[]) => void;
|
|
||||||
defaultSelection: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> = ({
|
|
||||||
columnDefinitions,
|
|
||||||
selectedColumnIds,
|
|
||||||
onSelectionChange,
|
|
||||||
defaultSelection,
|
|
||||||
}: TableColumnSelectionPaneProps): JSX.Element => {
|
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
|
||||||
const originalSelectedColumnIds = React.useMemo(() => selectedColumnIds, []);
|
|
||||||
const [columnSearchText, setColumnSearchText] = React.useState<string>("");
|
|
||||||
const [newSelectedColumnIds, setNewSelectedColumnIds] = React.useState<string[]>(originalSelectedColumnIds);
|
|
||||||
const styles = useColumnSelectionStyles();
|
|
||||||
|
|
||||||
const selectedColumnIdsSet = new Set(newSelectedColumnIds);
|
|
||||||
const onCheckedValueChange = (id: string, checkedData?: CheckboxOnChangeData): void => {
|
|
||||||
const checked = checkedData?.checked;
|
|
||||||
if (checked === "mixed" || checked === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
selectedColumnIdsSet.add(id);
|
|
||||||
} else {
|
|
||||||
/* selectedColumnIds may contain ids that are not in columnDefinitions, because the selected
|
|
||||||
* ids may have been loaded from persistence, but don't exist in the current retrieved documents.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (
|
|
||||||
Array.from(selectedColumnIdsSet).filter((id) => columnDefinitions.find((def) => def.id === id) !== undefined)
|
|
||||||
.length === 1 &&
|
|
||||||
selectedColumnIdsSet.has(id)
|
|
||||||
) {
|
|
||||||
// Don't allow unchecking the last column
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedColumnIdsSet.delete(id);
|
|
||||||
}
|
|
||||||
setNewSelectedColumnIds([...selectedColumnIdsSet]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSave = (): void => {
|
|
||||||
onSelectionChange(newSelectedColumnIds);
|
|
||||||
closeSidePanel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSearchChange: (event: SearchBoxChangeEvent, data: InputOnChangeData) => void = (_, data) =>
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
setColumnSearchText(data.value);
|
|
||||||
|
|
||||||
const theme = getPlatformTheme(configContext.platform);
|
|
||||||
|
|
||||||
// Filter and move partition keys to the top
|
|
||||||
const columnDefinitionList = columnDefinitions
|
|
||||||
.filter((def) => !columnSearchText || def.label.toLowerCase().includes(columnSearchText.toLowerCase()))
|
|
||||||
.sort((a, b) => {
|
|
||||||
const ID = "id";
|
|
||||||
// "id" always at the top, then partition keys, then everything else sorted
|
|
||||||
if (a.id === ID) {
|
|
||||||
return b.id === ID ? 0 : -1;
|
|
||||||
} else if (b.id === ID) {
|
|
||||||
return a.id === ID ? 0 : 1;
|
|
||||||
} else if (a.isPartitionKey && !b.isPartitionKey) {
|
|
||||||
return -1;
|
|
||||||
} else if (b.isPartitionKey && !a.isPartitionKey) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return a.label.localeCompare(b.label);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.paneContainer}>
|
|
||||||
<CosmosFluentProvider>
|
|
||||||
<div className="panelFormWrapper">
|
|
||||||
<div className="panelMainContent" style={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
<Text>Select which columns to display in your view of items in your container.</Text>
|
|
||||||
<div /* Wrap <SearchBox> to avoid margin-bottom set by panelMainContent css */>
|
|
||||||
<SearchBox
|
|
||||||
className={styles.searchBox}
|
|
||||||
value={columnSearchText}
|
|
||||||
onChange={onSearchChange}
|
|
||||||
placeholder="Search fields"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.checkboxContainer}>
|
|
||||||
{columnDefinitionList.map((columnDefinition) => (
|
|
||||||
<Checkbox
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
key={columnDefinition.id}
|
|
||||||
label={{
|
|
||||||
className: styles.checkboxLabel,
|
|
||||||
children: `${columnDefinition.label}${columnDefinition.isPartitionKey ? " (partition key)" : ""}`,
|
|
||||||
}}
|
|
||||||
checked={selectedColumnIdsSet.has(columnDefinition.id)}
|
|
||||||
onChange={(_, data) => onCheckedValueChange(columnDefinition.id, data)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Button appearance="secondary" size="small" onClick={() => setNewSelectedColumnIds(defaultSelection)}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="panelFooter" style={{ display: "flex", gap: theme.spacingHorizontalS }}>
|
|
||||||
<Button appearance="primary" onClick={onSave}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button appearance="secondary" onClick={closeSidePanel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CosmosFluentProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -171,7 +171,7 @@ export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
|
|||||||
the query builder.
|
the query builder.
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
||||||
<Text style={{ fontSize: 13 }}>CopilotSampleDB</Text>
|
<Text style={{ fontSize: 13 }}>CopilotSampleDb</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
||||||
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import {
|
|||||||
SuggestedPrompt,
|
SuggestedPrompt,
|
||||||
getSampleDatabaseSuggestedPrompts,
|
getSampleDatabaseSuggestedPrompts,
|
||||||
getSuggestedPrompts,
|
getSuggestedPrompts,
|
||||||
readPromptHistory,
|
|
||||||
savePromptHistory,
|
|
||||||
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
@@ -138,7 +136,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const [histories, setHistories] = useState<string[]>(() => readPromptHistory(userContext.databaseAccount));
|
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
||||||
|
const cachedHistories = cachedHistoriesString?.split("|");
|
||||||
|
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
||||||
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
||||||
? getSampleDatabaseSuggestedPrompts()
|
? getSampleDatabaseSuggestedPrompts()
|
||||||
: getSuggestedPrompts();
|
: getSuggestedPrompts();
|
||||||
@@ -172,7 +172,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||||
|
|
||||||
setHistories(newHistories);
|
setHistories(newHistories);
|
||||||
savePromptHistory(userContext.databaseAccount, newHistories);
|
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetMessageStates = (): void => {
|
const resetMessageStates = (): void => {
|
||||||
|
|||||||
@@ -1,39 +1,10 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { updateUserContext } from "UserContext";
|
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { QueryCopilotTab } from "./QueryCopilotTab";
|
import { QueryCopilotTab } from "./QueryCopilotTab";
|
||||||
|
|
||||||
describe("Query copilot tab snapshot test", () => {
|
describe("Query copilot tab snapshot test", () => {
|
||||||
it("should render with initial input", () => {
|
it("should render with initial input", () => {
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
name: "name",
|
|
||||||
properties: undefined,
|
|
||||||
id: "",
|
|
||||||
location: "",
|
|
||||||
type: "",
|
|
||||||
kind: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadState = (path: StorePath) => {
|
|
||||||
if (
|
|
||||||
path.componentName === AppStateComponentNames.QueryCopilot &&
|
|
||||||
path.subComponentName === CopilotSubComponentNames.toggleStatus
|
|
||||||
) {
|
|
||||||
return { enabled: true };
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
|
||||||
loadState,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { Stack } from "@fluentui/react";
|
|||||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||||
@@ -19,13 +18,18 @@ import SplitterLayout from "react-splitter-layout";
|
|||||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||||
|
import * as StringUtility from "../../Shared/StringUtility";
|
||||||
|
|
||||||
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
||||||
|
|
||||||
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
||||||
readCopilotToggleStatus(userContext.databaseAccount),
|
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
||||||
);
|
);
|
||||||
|
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
||||||
|
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
||||||
|
: true;
|
||||||
|
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
||||||
const [tabActive, setTabActive] = useState<boolean>(true);
|
const [tabActive, setTabActive] = useState<boolean>(true);
|
||||||
|
|
||||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
@@ -84,7 +88,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
|
|
||||||
const toggleCopilot = (toggle: boolean) => {
|
const toggleCopilot = (toggle: boolean) => {
|
||||||
setCopilotActive(toggle);
|
setCopilotActive(toggle);
|
||||||
saveCopilotToggleStatus(userContext.databaseAccount, toggle);
|
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, toggle.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
|
|
||||||
// Mock the items.query method to return the mockResult
|
// Mock the items.query method to return the mockResult
|
||||||
(
|
(
|
||||||
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").items.query as jest.Mock
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").items.query as jest.Mock
|
||||||
).mockReturnValue(mockResult);
|
).mockReturnValue(mockResult);
|
||||||
|
|
||||||
const result = querySampleDocuments(query, options);
|
const result = querySampleDocuments(query, options);
|
||||||
@@ -119,10 +119,10 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
const result = await readSampleDocument(documentId);
|
const result = await readSampleDocument(documentId);
|
||||||
|
|
||||||
expect(sampleDataClient).toHaveBeenCalled();
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDB");
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
||||||
expect(sampleDataClient().database("CopilotSampleDB").container).toHaveBeenCalledWith("SampleContainer");
|
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
expect(
|
expect(
|
||||||
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").item("DocumentId", undefined).read,
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read,
|
||||||
).toHaveBeenCalled();
|
).toHaveBeenCalled();
|
||||||
expect(result).toEqual(expectedResponse);
|
expect(result).toEqual(expectedResponse);
|
||||||
});
|
});
|
||||||
@@ -144,10 +144,10 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
|
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
|
||||||
|
|
||||||
expect(sampleDataClient).toHaveBeenCalled();
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDB");
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
||||||
expect(sampleDataClient().database("CopilotSampleDB").container).toHaveBeenCalledWith("SampleContainer");
|
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
expect(
|
expect(
|
||||||
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").item("DocumentId", undefined).read,
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read,
|
||||||
).toHaveBeenCalled();
|
).toHaveBeenCalled();
|
||||||
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
|
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import { handleError } from "Common/ErrorHandlingUtils";
|
|||||||
import { sampleDataClient } from "Common/SampleDataClient";
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
||||||
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||||
import { DatabaseAccount } from "Contracts/DataModels";
|
|
||||||
import DocumentId from "Explorer/Tree/DocumentId";
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
import { AppStateComponentNames, loadState, saveState } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
import * as StringUtility from "../../Shared/StringUtility";
|
|
||||||
|
|
||||||
export interface SuggestedPrompt {
|
export interface SuggestedPrompt {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -57,110 +54,3 @@ export const getSuggestedPrompts = (): SuggestedPrompt[] => {
|
|||||||
{ id: 3, text: "Find the oldest item added to my collection" },
|
{ id: 3, text: "Find the oldest item added to my collection" },
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prompt history persistence
|
|
||||||
export enum CopilotSubComponentNames {
|
|
||||||
promptHistory = "PromptHistory",
|
|
||||||
toggleStatus = "ToggleStatus",
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLegacyHistoryKey = (databaseAccount: DatabaseAccount): string =>
|
|
||||||
`${databaseAccount?.id}-queryCopilotHistories`;
|
|
||||||
const getLegacyToggleStatusKey = (databaseAccount: DatabaseAccount): string =>
|
|
||||||
`${databaseAccount?.id}-queryCopilotToggleStatus`;
|
|
||||||
|
|
||||||
// Migration only needs to run once
|
|
||||||
let hasMigrated = false;
|
|
||||||
// Migrate old prompt history to new format
|
|
||||||
export const migrateCopilotPersistence = (databaseAccount: DatabaseAccount): void => {
|
|
||||||
if (hasMigrated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = getLegacyHistoryKey(databaseAccount);
|
|
||||||
let item = localStorage.getItem(key);
|
|
||||||
if (item !== undefined && item !== null) {
|
|
||||||
const historyItems = item.split("|");
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.promptHistory,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
},
|
|
||||||
historyItems,
|
|
||||||
);
|
|
||||||
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
key = getLegacyToggleStatusKey(databaseAccount);
|
|
||||||
item = localStorage.getItem(key);
|
|
||||||
if (item !== undefined && item !== null) {
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.toggleStatus,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
},
|
|
||||||
StringUtility.toBoolean(item),
|
|
||||||
);
|
|
||||||
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMigrated = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const readPromptHistory = (databaseAccount: DatabaseAccount): string[] => {
|
|
||||||
migrateCopilotPersistence(databaseAccount);
|
|
||||||
return (
|
|
||||||
(loadState({
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.promptHistory,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
}) as string[]) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const savePromptHistory = (databaseAccount: DatabaseAccount, historyItems: string[]): void => {
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.promptHistory,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
},
|
|
||||||
historyItems,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const readCopilotToggleStatus = (databaseAccount: DatabaseAccount): boolean => {
|
|
||||||
migrateCopilotPersistence(databaseAccount);
|
|
||||||
return !!loadState({
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.toggleStatus,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
}) as boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveCopilotToggleStatus = (databaseAccount: DatabaseAccount, status: boolean): void => {
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.toggleStatus,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
},
|
|
||||||
status,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
|
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { querySampleDocuments, readCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -36,6 +36,7 @@ import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
|||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
import * as StringUtility from "../../../Shared/StringUtility";
|
||||||
|
|
||||||
async function fetchWithTimeout(
|
async function fetchWithTimeout(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -360,7 +361,9 @@ export const QueryDocumentsPerPage = async (
|
|||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isCopilotActive = readCopilotToggleStatus(userContext.databaseAccount);
|
const isCopilotActive = StringUtility.toBoolean(
|
||||||
|
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`),
|
||||||
|
);
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
|||||||
@@ -17,6 +17,38 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<QueryCopilotPromptbar
|
||||||
|
containerId="SampleContainer"
|
||||||
|
databaseId="CopilotSampleDb"
|
||||||
|
explorer={
|
||||||
|
Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
|
"isTabsContentExpanded": [Function],
|
||||||
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"armResourceId": undefined,
|
||||||
|
"retryOptions": {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provideFeedbackEmail": [Function],
|
||||||
|
"queriesClient": QueriesClient {
|
||||||
|
"container": [Circular],
|
||||||
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
|
"resourceTree": ResourceTreeAdapter {
|
||||||
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
|
"parameters": [Function],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleCopilot={[Function]}
|
||||||
|
/>
|
||||||
<Stack
|
<Stack
|
||||||
className="tabPaneContentContainer"
|
className="tabPaneContentContainer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
|
||||||
MenuButtonProps,
|
MenuButtonProps,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
@@ -26,7 +25,7 @@ import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
|||||||
import { Allotment, AllotmentHandle } from "allotment";
|
import { Allotment, AllotmentHandle } from "allotment";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
|
|
||||||
const useSidebarStyles = makeStyles({
|
const useSidebarStyles = makeStyles({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@@ -61,7 +60,6 @@ const useSidebarStyles = makeStyles({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyItems: "center",
|
justifyItems: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
containerType: "size", // Use this container for "@container" queries below this.
|
|
||||||
...cosmosShorthands.borderBottom(),
|
...cosmosShorthands.borderBottom(),
|
||||||
},
|
},
|
||||||
loadingProgressBar: {
|
loadingProgressBar: {
|
||||||
@@ -85,18 +83,6 @@ const useSidebarStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
globalCommandsMenuButton: {
|
|
||||||
display: "inline-flex",
|
|
||||||
"@container (min-width: 250px)": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
globalCommandsSplitButton: {
|
|
||||||
display: "none",
|
|
||||||
"@container (min-width: 250px)": {
|
|
||||||
display: "flex",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface GlobalCommandsProps {
|
interface GlobalCommandsProps {
|
||||||
@@ -113,12 +99,6 @@ interface GlobalCommand {
|
|||||||
|
|
||||||
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||||
const styles = useSidebarStyles();
|
const styles = useSidebarStyles();
|
||||||
|
|
||||||
// Since we have two buttons in the DOM (one for small screens and one for larger screens), we wrap the entire thing in a div.
|
|
||||||
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
|
|
||||||
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
|
|
||||||
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const actions = useMemo<GlobalCommand[]>(() => {
|
const actions = useMemo<GlobalCommand[]>(() => {
|
||||||
if (
|
if (
|
||||||
configContext.platform === Platform.Fabric ||
|
configContext.platform === Platform.Fabric ||
|
||||||
@@ -188,22 +168,16 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
{primaryAction.label}
|
{primaryAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Menu positioning={{ target: globalCommandButton, position: "below", align: "end" }}>
|
<Menu positioning="below-end">
|
||||||
<MenuTrigger disableButtonEnhancement>
|
<MenuTrigger disableButtonEnhancement>
|
||||||
{(triggerProps: MenuButtonProps) => (
|
{(triggerProps: MenuButtonProps) => (
|
||||||
<div ref={setGlobalCommandButton}>
|
|
||||||
<SplitButton
|
<SplitButton
|
||||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||||
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
||||||
className={styles.globalCommandsSplitButton}
|
|
||||||
icon={primaryAction.icon}
|
icon={primaryAction.icon}
|
||||||
>
|
>
|
||||||
{primaryAction.label}
|
{primaryAction.label}
|
||||||
</SplitButton>
|
</SplitButton>
|
||||||
<MenuButton {...triggerProps} icon={primaryAction.icon} className={styles.globalCommandsMenuButton}>
|
|
||||||
New...
|
|
||||||
</MenuButton>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuPopover>
|
<MenuPopover>
|
||||||
@@ -225,7 +199,7 @@ interface SidebarProps {
|
|||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollapseThreshold = 140;
|
const CollapseThreshold = 50;
|
||||||
|
|
||||||
export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||||
const styles = useSidebarStyles();
|
const styles = useSidebarStyles();
|
||||||
@@ -286,7 +260,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
{/* Collections Tree - Start */}
|
{/* Collections Tree - Start */}
|
||||||
{hasSidebar && (
|
{hasSidebar && (
|
||||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||||
<Allotment.Pane minSize={24} preferredSize={250}>
|
<Allotment.Pane minSize={24} preferredSize={300}>
|
||||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||||
<div className={styles.sidebarContainer}>
|
<div className={styles.sidebarContainer}>
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -340,7 +314,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
)}
|
)}
|
||||||
<Allotment.Pane minSize={200}>
|
<Allotment.Pane minSize={800}>
|
||||||
<Tabs explorer={explorer} />
|
<Tabs explorer={explorer} />
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
</Allotment>
|
</Allotment>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private clearMostRecent = (): void => {
|
private clearMostRecent = (): void => {
|
||||||
MostRecentActivity.clear(userContext.databaseAccount?.name);
|
MostRecentActivity.mostRecentActivity.clear(userContext.databaseAccount?.id);
|
||||||
this.setState({});
|
this.setState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -498,7 +498,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createRecentItems(): SplashScreenItem[] {
|
private createRecentItems(): SplashScreenItem[] {
|
||||||
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => {
|
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
default: {
|
default: {
|
||||||
const unknownActivity: never = activity;
|
const unknownActivity: never = activity;
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ export const htmlAttributeNames = {
|
|||||||
dataTableContentTypeAttr: "contentType_attr",
|
dataTableContentTypeAttr: "contentType_attr",
|
||||||
dataTableSnapshotAttr: "snapshot_attr",
|
dataTableSnapshotAttr: "snapshot_attr",
|
||||||
dataTableRowKeyAttr: "rowKey_attr",
|
dataTableRowKeyAttr: "rowKey_attr",
|
||||||
dataTablePartitionKeyAttr: "partKey_attr",
|
|
||||||
dataTableMessageIdAttr: "messageId_attr",
|
dataTableMessageIdAttr: "messageId_attr",
|
||||||
dataTableHeaderIndex: "data-column-index",
|
dataTableHeaderIndex: "data-column-index",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -193,9 +193,6 @@ function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: an
|
|||||||
* from UI elements.
|
* from UI elements.
|
||||||
*/
|
*/
|
||||||
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
|
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
|
||||||
if (aData.PartitionKey && aData.PartitionKey._) {
|
|
||||||
$(nRow).attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr, aData.PartitionKey._);
|
|
||||||
}
|
|
||||||
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
|
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
|
||||||
return nRow;
|
return nRow;
|
||||||
}
|
}
|
||||||
@@ -208,10 +205,6 @@ function selectionChanged(element: any, valueAccessor: any, allBindings: any, vi
|
|||||||
selected &&
|
selected &&
|
||||||
selected.forEach((b: Entities.ITableEntity) => {
|
selected.forEach((b: Entities.ITableEntity) => {
|
||||||
var sel = DataTableOperations.getRowSelector([
|
var sel = DataTableOperations.getRowSelector([
|
||||||
{
|
|
||||||
key: Constants.htmlAttributeNames.dataTablePartitionKeyAttr,
|
|
||||||
value: b.PartitionKey && b.PartitionKey._ && b.PartitionKey._.toString(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
|
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
|
||||||
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
|
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
|
||||||
@@ -377,9 +370,8 @@ function updateSelectionStatus(oSettings: any): void {
|
|||||||
for (var i = 0; i < $dataTableRows.length; i++) {
|
for (var i = 0; i < $dataTableRows.length; i++) {
|
||||||
var $row: JQuery = $dataTableRows.eq(i);
|
var $row: JQuery = $dataTableRows.eq(i);
|
||||||
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
|
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
|
||||||
var partitionKey: string = $row.attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr);
|
|
||||||
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
|
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
|
||||||
if (table.isItemSelected(table.getTableEntityKeys(rowKey, partitionKey))) {
|
if (table.isItemSelected(table.getTableEntityKeys(rowKey))) {
|
||||||
$row.attr("tabindex", "0");
|
$row.attr("tabindex", "0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,10 +56,7 @@ export default class DataTableOperationManager {
|
|||||||
// Simply select the first item in this case.
|
// Simply select the first item in this case.
|
||||||
var lastSelectedItemIndex = lastSelectedItem
|
var lastSelectedItemIndex = lastSelectedItem
|
||||||
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(
|
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._),
|
||||||
lastSelectedItem.RowKey._,
|
|
||||||
lastSelectedItem.PartitionKey && lastSelectedItem.PartitionKey._,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: -1;
|
: -1;
|
||||||
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
||||||
@@ -150,14 +147,13 @@ export default class DataTableOperationManager {
|
|||||||
private getEntityIdentity($elem: JQuery<Element>): Entities.ITableEntityIdentity {
|
private getEntityIdentity($elem: JQuery<Element>): Entities.ITableEntityIdentity {
|
||||||
return {
|
return {
|
||||||
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
||||||
PartitionKey: $elem.attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
|
private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
this._tableEntityListViewModel.lastSelectedItem = entity;
|
this._tableEntityListViewModel.lastSelectedItem = entity;
|
||||||
@@ -172,7 +168,7 @@ export default class DataTableOperationManager {
|
|||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
|
|
||||||
this._tableEntityListViewModel.clearSelection();
|
this._tableEntityListViewModel.clearSelection();
|
||||||
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
this.addToSelection(entityIdentity.RowKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +190,11 @@ export default class DataTableOperationManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
!this._tableEntityListViewModel.isItemSelected(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Adding item not previously in selection
|
// Adding item not previously in selection
|
||||||
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
this.addToSelection(entityIdentity.RowKey);
|
||||||
} else {
|
} else {
|
||||||
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
||||||
}
|
}
|
||||||
@@ -216,10 +212,10 @@ export default class DataTableOperationManager {
|
|||||||
if (anchorItem) {
|
if (anchorItem) {
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
||||||
);
|
);
|
||||||
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.PartitionKey._, anchorItem.RowKey._),
|
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._),
|
||||||
);
|
);
|
||||||
|
|
||||||
var startIndex = Math.min(elementIndex, anchorIndex);
|
var startIndex = Math.min(elementIndex, anchorIndex);
|
||||||
@@ -238,25 +234,24 @@ export default class DataTableOperationManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
!this._tableEntityListViewModel.isItemSelected(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (this._tableEntityListViewModel.selected().length) {
|
if (this._tableEntityListViewModel.selected().length) {
|
||||||
this._tableEntityListViewModel.clearSelection();
|
this._tableEntityListViewModel.clearSelection();
|
||||||
}
|
}
|
||||||
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
this.addToSelection(entityIdentity.RowKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addToSelection(rowKey: string, partitionKey?: string) {
|
private addToSelection(rowKey: string) {
|
||||||
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(rowKey, partitionKey),
|
this._tableEntityListViewModel.getTableEntityKeys(rowKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedEntity != null) {
|
if (selectedEntity != null) {
|
||||||
this._tableEntityListViewModel.selected.push(selectedEntity);
|
this._tableEntityListViewModel.selected.push(selectedEntity);
|
||||||
}
|
}
|
||||||
console.log(this._tableEntityListViewModel.selected().length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selecting first row if the selection is empty.
|
// Selecting first row if the selection is empty.
|
||||||
@@ -274,7 +269,7 @@ export default class DataTableOperationManager {
|
|||||||
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
||||||
this._tableEntityListViewModel.clearLastSelected();
|
this._tableEntityListViewModel.clearLastSelected();
|
||||||
|
|
||||||
this.addToSelection(firstEntity.RowKey._, firstEntity.PartitionKey && firstEntity.PartitionKey._);
|
this.addToSelection(firstEntity.RowKey._);
|
||||||
|
|
||||||
// Update last selection
|
// Update last selection
|
||||||
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
||||||
|
|||||||
@@ -128,14 +128,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
this.sqlQuery = ko.observable<string>("SELECT * FROM c");
|
this.sqlQuery = ko.observable<string>("SELECT * FROM c");
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTableEntityKeys(rowKey: string, partitionKey: string): Entities.IProperty[] {
|
public getTableEntityKeys(rowKey: string): Entities.IProperty[] {
|
||||||
const properties: Entities.IProperty[] = [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
||||||
|
|
||||||
if (partitionKey) {
|
|
||||||
properties.push({ key: Constants.EntityKeyNames.PartitionKey, value: partitionKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
return properties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.Api<Element> {
|
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.Api<Element> {
|
||||||
@@ -267,8 +261,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
}
|
}
|
||||||
var oldEntityIndex: number = _.findIndex(
|
var oldEntityIndex: number = _.findIndex(
|
||||||
this.cache.data,
|
this.cache.data,
|
||||||
(data: Entities.ITableEntity) =>
|
(data: Entities.ITableEntity) => data.RowKey._ === entity.RowKey._,
|
||||||
data.RowKey._ === entity.RowKey._ && data.PartitionKey._ === entity.PartitionKey._,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cache.data.splice(oldEntityIndex, 1, entity);
|
this.cache.data.splice(oldEntityIndex, 1, entity);
|
||||||
@@ -292,7 +285,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
entities.forEach((entity: Entities.ITableEntity) => {
|
entities.forEach((entity: Entities.ITableEntity) => {
|
||||||
var cachedIndex: number = _.findIndex(
|
var cachedIndex: number = _.findIndex(
|
||||||
this.cache.data,
|
this.cache.data,
|
||||||
(e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._ && e.PartitionKey._ === entity.PartitionKey._,
|
(e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._,
|
||||||
);
|
);
|
||||||
if (cachedIndex >= 0) {
|
if (cachedIndex >= 0) {
|
||||||
this.cache.data.splice(cachedIndex, 1);
|
this.cache.data.splice(cachedIndex, 1);
|
||||||
@@ -400,16 +393,6 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override as Tables can have the same Row key in different Partition keys
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
|
||||||
return _.find(this.items(), (item: Entities.ITableEntity) => {
|
|
||||||
return this.matchesKeys(item, itemKeys);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private prefetchAndRender(
|
private prefetchAndRender(
|
||||||
tableQuery: Entities.ITableQuery,
|
tableQuery: Entities.ITableQuery,
|
||||||
tablePageStartIndex: number,
|
tablePageStartIndex: number,
|
||||||
|
|||||||
@@ -36,5 +36,4 @@ export interface ITableQuery {
|
|||||||
|
|
||||||
export interface ITableEntityIdentity {
|
export interface ITableEntityIdentity {
|
||||||
RowKey: string;
|
RowKey: string;
|
||||||
PartitionKey?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -753,11 +753,17 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
CassandraProxyEndpoints.Development,
|
CassandraProxyEndpoints.Development,
|
||||||
CassandraProxyEndpoints.Mpac,
|
CassandraProxyEndpoints.Mpac,
|
||||||
CassandraProxyEndpoints.Prod,
|
CassandraProxyEndpoints.Prod,
|
||||||
CassandraProxyEndpoints.Fairfax,
|
|
||||||
CassandraProxyEndpoints.Mooncake,
|
|
||||||
];
|
];
|
||||||
|
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||||
|
if (
|
||||||
|
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
|
||||||
|
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||||
|
) {
|
||||||
|
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
canAccessCassandraProxy &&
|
||||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
// Definitions of State data
|
|
||||||
|
|
||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
|
||||||
import {
|
|
||||||
AppStateComponentNames,
|
|
||||||
deleteState,
|
|
||||||
loadState,
|
|
||||||
saveState,
|
|
||||||
saveStateDebounced,
|
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
|
|
||||||
const componentName = AppStateComponentNames.DocumentsTab;
|
|
||||||
|
|
||||||
export enum SubComponentName {
|
|
||||||
ColumnSizes = "ColumnSizes",
|
|
||||||
FilterHistory = "FilterHistory",
|
|
||||||
MainTabDivider = "MainTabDivider",
|
|
||||||
ColumnsSelection = "ColumnsSelection",
|
|
||||||
ColumnSort = "ColumnSort",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
|
||||||
export type FilterHistory = string[];
|
|
||||||
export type WidthDefinition = { widthPx: number };
|
|
||||||
export type TabDivider = { leftPaneWidthPercent: number };
|
|
||||||
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
|
||||||
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param subComponentName
|
|
||||||
* @param collection
|
|
||||||
* @param defaultValue Will be returned if persisted state is not found
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const readSubComponentState = <T>(
|
|
||||||
subComponentName: SubComponentName,
|
|
||||||
collection: ViewModels.CollectionBase,
|
|
||||||
defaultValue: T,
|
|
||||||
): T => {
|
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = loadState({
|
|
||||||
componentName: componentName,
|
|
||||||
subComponentName,
|
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
}) as T;
|
|
||||||
|
|
||||||
return state || defaultValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param subComponentName
|
|
||||||
* @param collection
|
|
||||||
* @param state State to save
|
|
||||||
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
|
||||||
*/
|
|
||||||
export const saveSubComponentState = <T>(
|
|
||||||
subComponentName: SubComponentName,
|
|
||||||
collection: ViewModels.CollectionBase,
|
|
||||||
state: T,
|
|
||||||
debounce?: boolean,
|
|
||||||
): void => {
|
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(debounce ? saveStateDebounced : saveState)(
|
|
||||||
{
|
|
||||||
componentName: componentName,
|
|
||||||
subComponentName,
|
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
},
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSubComponentState = (subComponentName: SubComponentName, collection: ViewModels.CollectionBase) => {
|
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteState({
|
|
||||||
componentName: componentName,
|
|
||||||
subComponentName,
|
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
||||||
import { waitFor } from "@testing-library/react";
|
|
||||||
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import {
|
import {
|
||||||
ButtonsDependencies,
|
ButtonsDependencies,
|
||||||
DELETE_BUTTON_ID,
|
DELETE_BUTTON_ID,
|
||||||
@@ -16,7 +13,6 @@ import {
|
|||||||
SAVE_BUTTON_ID,
|
SAVE_BUTTON_ID,
|
||||||
UPDATE_BUTTON_ID,
|
UPDATE_BUTTON_ID,
|
||||||
UPLOAD_BUTTON_ID,
|
UPLOAD_BUTTON_ID,
|
||||||
addStringsNoDuplicate,
|
|
||||||
buildQuery,
|
buildQuery,
|
||||||
getDiscardExistingDocumentChangesButtonState,
|
getDiscardExistingDocumentChangesButtonState,
|
||||||
getDiscardNewDocumentChangesButtonState,
|
getDiscardNewDocumentChangesButtonState,
|
||||||
@@ -68,14 +64,12 @@ jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
|||||||
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
|
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockDialogState = {
|
|
||||||
showOkCancelModalDialog: jest.fn((title: string, subText: string, okLabel: string, onOk: () => void) => onOk()),
|
|
||||||
showOkModalDialog: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Dialog", () => ({
|
jest.mock("Explorer/Controls/Dialog", () => ({
|
||||||
useDialog: {
|
useDialog: {
|
||||||
getState: jest.fn(() => mockDialogState),
|
getState: jest.fn(() => ({
|
||||||
|
showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(),
|
||||||
|
showOkModalDialog: () => {},
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -85,10 +79,6 @@ jest.mock("Common/dataAccess/deleteDocument", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/ProgressModalDialog", () => ({
|
|
||||||
ProgressModalDialog: jest.fn(() => <></>),
|
|
||||||
}));
|
|
||||||
|
|
||||||
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
||||||
let newWrapper;
|
let newWrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -101,13 +91,7 @@ async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | S
|
|||||||
describe("Documents tab (noSql API)", () => {
|
describe("Documents tab (noSql API)", () => {
|
||||||
describe("buildQuery", () => {
|
describe("buildQuery", () => {
|
||||||
it("should generate the right select query for SQL API", () => {
|
it("should generate the right select query for SQL API", () => {
|
||||||
expect(
|
expect(buildQuery(false, "")).toContain("select");
|
||||||
buildQuery(false, "", ["pk"], {
|
|
||||||
paths: ["pk"],
|
|
||||||
kind: "Hash",
|
|
||||||
version: 2,
|
|
||||||
}),
|
|
||||||
).toContain("select");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -355,10 +339,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
const createMockProps = (): IDocumentsTabComponentProps => ({
|
const createMockProps = (): IDocumentsTabComponentProps => ({
|
||||||
isPreferredApiMongoDB: false,
|
isPreferredApiMongoDB: false,
|
||||||
documentIds: [],
|
documentIds: [],
|
||||||
collection: {
|
collection: undefined,
|
||||||
id: ko.observable<string>("collectionId"),
|
|
||||||
databaseId: "databaseId",
|
|
||||||
} as ViewModels.CollectionBase,
|
|
||||||
partitionKey: { kind: "Hash", paths: ["/foo"], version: 2 },
|
partitionKey: { kind: "Hash", paths: ["/foo"], version: 2 },
|
||||||
onLoadStartKey: 0,
|
onLoadStartKey: 0,
|
||||||
tabTitle: "",
|
tabTitle: "",
|
||||||
@@ -399,7 +380,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
.findWhere((node) => node.text() === "Edit Filter")
|
.findWhere((node) => node.text() === "Edit Filter")
|
||||||
.at(0)
|
.at(0)
|
||||||
.simulate("click");
|
.simulate("click");
|
||||||
expect(wrapper.find("Input.filterInput").exists()).toBeTruthy();
|
expect(wrapper.find("#filterInput").exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -461,7 +442,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -471,36 +452,14 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking Delete Document asks for confirmation", async () => {
|
it("clicking Delete Document asks for confirmation", () => {
|
||||||
act(async () => {
|
|
||||||
await useCommandBar
|
|
||||||
.getState()
|
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
|
||||||
.onCommandClick(undefined, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(useDialog.getState().showOkCancelModalDialog).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clicking Delete Document for NoSql shows progress dialog", () => {
|
|
||||||
act(() => {
|
|
||||||
useCommandBar
|
|
||||||
.getState()
|
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
|
||||||
.onCommandClick(undefined, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ProgressModalDialog).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clicking Delete Document eventually calls delete client api", () => {
|
|
||||||
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
||||||
mockDeleteDocuments.mockClear();
|
mockDeleteDocuments.mockClear();
|
||||||
|
|
||||||
@@ -508,21 +467,10 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The implementation uses setTimeout, so wait for it to finish
|
expect(mockDeleteDocuments).toHaveBeenCalled();
|
||||||
waitFor(() => expect(mockDeleteDocuments).toHaveBeenCalled());
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Documents tab", () => {
|
|
||||||
it("should add strings to array without duplicate", () => {
|
|
||||||
const array1 = ["a", "b", "c"];
|
|
||||||
const array2 = ["b", "c", "d"];
|
|
||||||
|
|
||||||
const array3 = addStringsNoDuplicate(array1, array2);
|
|
||||||
expect(array3).toEqual(["a", "b", "c", "d"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import { deleteDocuments } from "Common/MongoProxyClient";
|
import { deleteDocument } from "Common/MongoProxyClient";
|
||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import {
|
import {
|
||||||
DELETE_BUTTON_ID,
|
DELETE_BUTTON_ID,
|
||||||
DISCARD_BUTTON_ID,
|
DISCARD_BUTTON_ID,
|
||||||
@@ -49,9 +49,7 @@ jest.mock("Common/MongoProxyClient", () => ({
|
|||||||
id: "id1",
|
id: "id1",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
deleteDocuments: jest.fn(() => Promise.resolve({ deleteCount: 0, isAcknowledged: true })),
|
deleteDocument: jest.fn(() => Promise.resolve()),
|
||||||
ThrottlingError: Error,
|
|
||||||
useMongoProxyEndpoint: jest.fn(() => true),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
@@ -163,7 +161,7 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -173,25 +171,25 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking Delete Document eventually calls delete client api", () => {
|
it("clicking Delete Document asks for confirmation", () => {
|
||||||
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
const mockDeleteDocument = deleteDocument as jest.Mock;
|
||||||
mockDeleteDocuments.mockClear();
|
mockDeleteDocument.mockClear();
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDeleteDocuments).toHaveBeenCalled();
|
expect(mockDeleteDocument).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TableRowId } from "@fluentui/react-components";
|
import { TableRowId } from "@fluentui/react-components";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import { DocumentsTableComponent, IDocumentsTableComponentProps } from "./DocumentsTableComponent";
|
import { DocumentsTableComponent, IDocumentsTableComponentProps } from "./DocumentsTableComponent";
|
||||||
|
|
||||||
const PARTITION_KEY_HEADER = "partitionKey";
|
const PARTITION_KEY_HEADER = "partitionKey";
|
||||||
@@ -21,19 +20,11 @@ describe("DocumentsTableComponent", () => {
|
|||||||
height: 0,
|
height: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
},
|
},
|
||||||
columnDefinitions: [
|
columnHeaders: {
|
||||||
{ id: ID_HEADER, label: "ID", isPartitionKey: false },
|
idHeader: ID_HEADER,
|
||||||
{ id: PARTITION_KEY_HEADER, label: "Partition Key", isPartitionKey: true },
|
partitionKeyHeaders: [PARTITION_KEY_HEADER],
|
||||||
],
|
|
||||||
isRowSelectionDisabled: false,
|
|
||||||
collection: {
|
|
||||||
databaseId: "db",
|
|
||||||
id: ((): string => "coll") as ko.Observable<string>,
|
|
||||||
} as ViewModels.CollectionBase,
|
|
||||||
onRefreshTable: (): void => {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
},
|
||||||
selectedColumnIds: [],
|
isSelectionDisabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render documents and partition keys in header", () => {
|
it("should render documents and partition keys in header", () => {
|
||||||
@@ -44,7 +35,7 @@ describe("DocumentsTableComponent", () => {
|
|||||||
|
|
||||||
it("should not render selection column when isSelectionDisabled is true", () => {
|
it("should not render selection column when isSelectionDisabled is true", () => {
|
||||||
const props: IDocumentsTableComponentProps = createMockProps();
|
const props: IDocumentsTableComponentProps = createMockProps();
|
||||||
props.isRowSelectionDisabled = true;
|
props.isSelectionDisabled = true;
|
||||||
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,86 +1,52 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Menu,
|
Menu,
|
||||||
MenuDivider,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuPopover,
|
MenuPopover,
|
||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
TableRowData as RowStateBase,
|
TableRowData as RowStateBase,
|
||||||
SortDirection,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableCellLayout,
|
TableCellLayout,
|
||||||
TableColumnDefinition,
|
TableColumnDefinition,
|
||||||
TableColumnId,
|
|
||||||
TableColumnSizingOptions,
|
TableColumnSizingOptions,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableRowId,
|
TableRowId,
|
||||||
TableSelectionCell,
|
TableSelectionCell,
|
||||||
tokens,
|
createTableColumn,
|
||||||
useArrowNavigationGroup,
|
useArrowNavigationGroup,
|
||||||
useTableColumnSizing_unstable,
|
useTableColumnSizing_unstable,
|
||||||
useTableFeatures,
|
useTableFeatures,
|
||||||
useTableSelection,
|
useTableSelection,
|
||||||
useTableSort,
|
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import {
|
|
||||||
ArrowClockwise16Regular,
|
|
||||||
ArrowResetRegular,
|
|
||||||
DeleteRegular,
|
|
||||||
EditRegular,
|
|
||||||
MoreHorizontalRegular,
|
|
||||||
TableResizeColumnRegular,
|
|
||||||
TextSortAscendingRegular,
|
|
||||||
TextSortDescendingRegular,
|
|
||||||
} from "@fluentui/react-icons";
|
|
||||||
import { NormalizedEventKey } from "Common/Constants";
|
import { NormalizedEventKey } from "Common/Constants";
|
||||||
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
|
||||||
import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
|
||||||
import {
|
|
||||||
ColumnSizesMap,
|
|
||||||
ColumnSort,
|
|
||||||
deleteSubComponentState,
|
|
||||||
readSubComponentState,
|
|
||||||
saveSubComponentState,
|
|
||||||
SubComponentName,
|
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
|
||||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
export type DocumentsTableComponentItem = {
|
export type DocumentsTableComponentItem = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Record<string, string | number>;
|
} & Record<string, string>;
|
||||||
|
|
||||||
export type ColumnDefinition = {
|
export type ColumnHeaders = {
|
||||||
id: string;
|
idHeader: string;
|
||||||
label: string;
|
partitionKeyHeaders: string[];
|
||||||
isPartitionKey: boolean;
|
|
||||||
};
|
};
|
||||||
export interface IDocumentsTableComponentProps {
|
export interface IDocumentsTableComponentProps {
|
||||||
onRefreshTable: () => void;
|
|
||||||
items: DocumentsTableComponentItem[];
|
items: DocumentsTableComponentItem[];
|
||||||
onItemClicked: (index: number) => void;
|
onItemClicked: (index: number) => void;
|
||||||
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
||||||
selectedRows: Set<TableRowId>;
|
selectedRows: Set<TableRowId>;
|
||||||
size: { height: number; width: number };
|
size: { height: number; width: number };
|
||||||
selectedColumnIds: string[];
|
columnHeaders: ColumnHeaders;
|
||||||
columnDefinitions: ColumnDefinition[];
|
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
isRowSelectionDisabled?: boolean;
|
isSelectionDisabled?: boolean;
|
||||||
collection: ViewModels.CollectionBase;
|
|
||||||
onColumnSelectionChange?: (newSelectedColumnIds: string[]) => void;
|
|
||||||
defaultColumnSelection?: string[];
|
|
||||||
isColumnSelectionDisabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
||||||
@@ -93,204 +59,72 @@ interface ReactWindowRenderFnProps extends ListChildComponentProps {
|
|||||||
data: TableRowData[];
|
data: TableRowData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMNS_MENU_NAME = "columnsMenu";
|
|
||||||
|
|
||||||
const defaultSize = {
|
|
||||||
idealWidth: 200,
|
|
||||||
minWidth: 50,
|
|
||||||
};
|
|
||||||
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||||
onRefreshTable,
|
|
||||||
items,
|
items,
|
||||||
onSelectedRowsChange,
|
onSelectedRowsChange,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
style,
|
style,
|
||||||
size,
|
size,
|
||||||
selectedColumnIds,
|
columnHeaders,
|
||||||
columnDefinitions,
|
isSelectionDisabled,
|
||||||
isRowSelectionDisabled: isSelectionDisabled,
|
|
||||||
collection,
|
|
||||||
onColumnSelectionChange,
|
|
||||||
defaultColumnSelection,
|
|
||||||
isColumnSelectionDisabled,
|
|
||||||
}: IDocumentsTableComponentProps) => {
|
}: IDocumentsTableComponentProps) => {
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
const initialSizingOptions: TableColumnSizingOptions = {
|
||||||
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
id: {
|
||||||
const columnSizesPx: TableColumnSizingOptions = {};
|
idealWidth: 280,
|
||||||
selectedColumnIds.forEach((columnId) => {
|
minWidth: 50,
|
||||||
if (
|
},
|
||||||
!columnSizesMap ||
|
};
|
||||||
!columnSizesMap[columnId] ||
|
columnHeaders.partitionKeyHeaders.forEach((pkHeader) => {
|
||||||
columnSizesMap[columnId].widthPx === undefined ||
|
initialSizingOptions[pkHeader] = {
|
||||||
isNaN(columnSizesMap[columnId].widthPx)
|
idealWidth: 200,
|
||||||
) {
|
|
||||||
columnSizesPx[columnId] = defaultSize;
|
|
||||||
} else {
|
|
||||||
columnSizesPx[columnId] = {
|
|
||||||
idealWidth: columnSizesMap[columnId].widthPx,
|
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
});
|
|
||||||
return columnSizesPx;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sortState, setSortState] = React.useState<{
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(initialSizingOptions);
|
||||||
sortDirection: "ascending" | "descending";
|
|
||||||
sortColumn: TableColumnId | undefined;
|
|
||||||
}>(() => {
|
|
||||||
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
|
||||||
|
|
||||||
if (!sort) {
|
const onColumnResize = React.useCallback((_, { columnId, width }) => {
|
||||||
return {
|
setColumnSizingOptions((state) => ({
|
||||||
sortDirection: undefined,
|
|
||||||
sortColumn: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sortDirection: sort.direction,
|
|
||||||
sortColumn: sort.columnId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const onColumnResize = React.useCallback((_, { columnId, width }: { columnId: string; width: number }) => {
|
|
||||||
setColumnSizingOptions((state) => {
|
|
||||||
const newSizingOptions = {
|
|
||||||
...state,
|
...state,
|
||||||
[columnId]: {
|
[columnId]: {
|
||||||
...state[columnId],
|
...state[columnId],
|
||||||
idealWidth: width,
|
idealWidth: width,
|
||||||
},
|
},
|
||||||
};
|
}));
|
||||||
|
|
||||||
const persistentSizes = Object.keys(newSizingOptions).reduce((acc, key) => {
|
|
||||||
acc[key] = {
|
|
||||||
widthPx: newSizingOptions[key].idealWidth,
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
}, {} as ColumnSizesMap);
|
|
||||||
|
|
||||||
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
|
||||||
|
|
||||||
return newSizingOptions;
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const restoreFocusTargetAttribute = useRestoreFocusTarget();
|
|
||||||
|
|
||||||
const onSortClick = (event: React.SyntheticEvent, columnId: string, direction: SortDirection) => {
|
|
||||||
setColumnSort(event, columnId, direction);
|
|
||||||
|
|
||||||
if (columnId === undefined || direction === undefined) {
|
|
||||||
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||||
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
columnDefinitions
|
[
|
||||||
.filter((column) => selectedColumnIds.includes(column.id))
|
createTableColumn<DocumentsTableComponentItem>({
|
||||||
.map((column) => ({
|
columnId: "id",
|
||||||
columnId: column.id,
|
compare: (a, b) => a.id.localeCompare(b.id),
|
||||||
compare: (a, b) => {
|
renderHeaderCell: () => columnHeaders.idHeader,
|
||||||
if (typeof a[column.id] === "string") {
|
|
||||||
return (a[column.id] as string).localeCompare(b[column.id] as string);
|
|
||||||
} else if (typeof a[column.id] === "number") {
|
|
||||||
return (a[column.id] as number) - (b[column.id] as number);
|
|
||||||
} else {
|
|
||||||
// Should not happen
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderHeaderCell: () => (
|
|
||||||
<>
|
|
||||||
<span title={column.label}>{column.label}</span>
|
|
||||||
<Menu>
|
|
||||||
<MenuTrigger disableButtonEnhancement>
|
|
||||||
<Button
|
|
||||||
// {...restoreFocusTargetAttribute}
|
|
||||||
appearance="transparent"
|
|
||||||
aria-label="Select column"
|
|
||||||
size="small"
|
|
||||||
icon={<MoreHorizontalRegular />}
|
|
||||||
style={{ position: "absolute", right: 0, backgroundColor: tokens.colorNeutralBackground1 }}
|
|
||||||
/>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>
|
|
||||||
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
|
||||||
Refresh
|
|
||||||
</MenuItem>
|
|
||||||
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) && (
|
|
||||||
<>
|
|
||||||
<MenuItem
|
|
||||||
icon={<TextSortAscendingRegular />}
|
|
||||||
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
|
||||||
>
|
|
||||||
Sort ascending
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon={<TextSortDescendingRegular />}
|
|
||||||
onClick={(e) => onSortClick(e, column.id, "descending")}
|
|
||||||
>
|
|
||||||
Sort descending
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem icon={<ArrowResetRegular />} onClick={(e) => onSortClick(e, undefined, undefined)}>
|
|
||||||
Reset sorting
|
|
||||||
</MenuItem>
|
|
||||||
{!isColumnSelectionDisabled && (
|
|
||||||
<MenuItem key="editcolumns" icon={<EditRegular />} onClick={openColumnSelectionPane}>
|
|
||||||
Edit columns
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuDivider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<MenuItem
|
|
||||||
key="keyboardresize"
|
|
||||||
icon={<TableResizeColumnRegular />}
|
|
||||||
onClick={columnSizing.enableKeyboardMode(column.id)}
|
|
||||||
>
|
|
||||||
Resize with left/right arrow keys
|
|
||||||
</MenuItem>
|
|
||||||
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) &&
|
|
||||||
!isColumnSelectionDisabled && (
|
|
||||||
<MenuItem
|
|
||||||
key="remove"
|
|
||||||
icon={<DeleteRegular />}
|
|
||||||
onClick={() => {
|
|
||||||
// Remove column id from selectedColumnIds
|
|
||||||
const index = selectedColumnIds.indexOf(column.id);
|
|
||||||
if (index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newSelectedColumnIds = [...selectedColumnIds];
|
|
||||||
newSelectedColumnIds.splice(index, 1);
|
|
||||||
onColumnSelectionChange(newSelectedColumnIds);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove column
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
renderCell: (item) => (
|
renderCell: (item) => (
|
||||||
<TableCellLayout truncate title={`${item[column.id]}`}>
|
<TableCellLayout truncate title={item.id}>
|
||||||
{item[column.id]}
|
{item.id}
|
||||||
</TableCellLayout>
|
</TableCellLayout>
|
||||||
),
|
),
|
||||||
})),
|
}),
|
||||||
[columnDefinitions, onColumnSelectionChange, selectedColumnIds],
|
].concat(
|
||||||
|
columnHeaders.partitionKeyHeaders.map((pkHeader) =>
|
||||||
|
createTableColumn<DocumentsTableComponentItem>({
|
||||||
|
columnId: pkHeader,
|
||||||
|
compare: (a, b) => a[pkHeader].localeCompare(b[pkHeader]),
|
||||||
|
// Show Refresh button on last column
|
||||||
|
renderHeaderCell: () => <span title={pkHeader}>{pkHeader}</span>,
|
||||||
|
renderCell: (item) => (
|
||||||
|
<TableCellLayout truncate title={item[pkHeader]}>
|
||||||
|
{item[pkHeader]}
|
||||||
|
</TableCellLayout>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[columnHeaders],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
||||||
@@ -380,7 +214,6 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
columnSizing_unstable: columnSizing,
|
columnSizing_unstable: columnSizing,
|
||||||
tableRef,
|
tableRef,
|
||||||
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
||||||
sort: { getSortDirection, setColumnSort, sort },
|
|
||||||
} = useTableFeatures(
|
} = useTableFeatures(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
@@ -394,20 +227,10 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems),
|
onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems),
|
||||||
}),
|
}),
|
||||||
useTableSort({
|
|
||||||
sortState,
|
|
||||||
onSortChange: (e, nextSortState) => setSortState(nextSortState),
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const headerSortProps = (columnId: TableColumnId) => ({
|
const rows: TableRowData[] = getRows((row) => {
|
||||||
// onClick: (e: React.MouseEvent) => toggleColumnSort(e, columnId),
|
|
||||||
sortDirection: getSortDirection(columnId),
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows: TableRowData[] = sort(
|
|
||||||
getRows((row) => {
|
|
||||||
const selected = isRowSelected(row.rowId);
|
const selected = isRowSelected(row.rowId);
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
@@ -421,8 +244,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
selected,
|
selected,
|
||||||
appearance: selected ? ("brand" as const) : ("none" as const),
|
appearance: selected ? ("brand" as const) : ("none" as const),
|
||||||
};
|
};
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const toggleAllKeydown = React.useCallback(
|
const toggleAllKeydown = React.useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
@@ -449,53 +271,39 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
...style,
|
...style,
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkedValues: { [COLUMNS_MENU_NAME]: string[] } = {
|
|
||||||
[COLUMNS_MENU_NAME]: [],
|
|
||||||
};
|
|
||||||
columnDefinitions.forEach(
|
|
||||||
(columnDefinition) =>
|
|
||||||
selectedColumnIds.includes(columnDefinition.id) && checkedValues[COLUMNS_MENU_NAME].push(columnDefinition.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const openColumnSelectionPane = (): void => {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
"Select columns",
|
|
||||||
<TableColumnSelectionPane
|
|
||||||
selectedColumnIds={selectedColumnIds}
|
|
||||||
columnDefinitions={columnDefinitions}
|
|
||||||
onSelectionChange={onColumnSelectionChange}
|
|
||||||
defaultSelection={defaultColumnSelection}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table noNativeElements {...tableProps}>
|
<Table noNativeElements {...tableProps}>
|
||||||
<TableHeader className={styles.tableHeader}>
|
<TableHeader>
|
||||||
<TableRow className={styles.tableRow} style={{ width: size ? size.width - 15 : "100%" }}>
|
<TableRow className={styles.tableRow} style={{ width: size ? size.width - 15 : "100%" }}>
|
||||||
{!isSelectionDisabled && (
|
{!isSelectionDisabled && (
|
||||||
<TableSelectionCell
|
<TableSelectionCell
|
||||||
key="selectcell"
|
|
||||||
checked={allRowsSelected ? true : someRowsSelected ? "mixed" : false}
|
checked={allRowsSelected ? true : someRowsSelected ? "mixed" : false}
|
||||||
onClick={toggleAllRows}
|
onClick={toggleAllRows}
|
||||||
onKeyDown={toggleAllKeydown}
|
onKeyDown={toggleAllKeydown}
|
||||||
checkboxIndicator={{ "aria-label": "Select all rows " }}
|
checkboxIndicator={{ "aria-label": "Select all rows " }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{columns.map((column) => (
|
{columns.map((column /* index */) => (
|
||||||
|
<Menu openOnContext key={column.columnId}>
|
||||||
|
<MenuTrigger>
|
||||||
<TableHeaderCell
|
<TableHeaderCell
|
||||||
className={styles.tableCell}
|
className={styles.tableCell}
|
||||||
key={column.columnId}
|
key={column.columnId}
|
||||||
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
||||||
{...headerSortProps(column.columnId)}
|
|
||||||
>
|
>
|
||||||
{column.renderHeaderCell()}
|
{column.renderHeaderCell()}
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuPopover>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={columnSizing.enableKeyboardMode(column.columnId)}>
|
||||||
|
Keyboard Column Resizing
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<div className={styles.tableHeaderFiller}></div>
|
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<List
|
<List
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to help with selection.
|
* Utility class to help with selection.
|
||||||
* This emulates File Explorer selection behavior.
|
* This emulates File Explorer selection behavior.
|
||||||
@@ -92,12 +90,3 @@ export const selectionHelper = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// To get previous values of a state in useEffect
|
|
||||||
export const usePrevious = <T>(value: T): T | undefined => {
|
|
||||||
const ref = useRef<T>();
|
|
||||||
useEffect(() => {
|
|
||||||
ref.current = value;
|
|
||||||
});
|
|
||||||
return ref.current;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -38,11 +38,9 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Allotment
|
<Allotment>
|
||||||
onDragEnd={[Function]}
|
|
||||||
>
|
|
||||||
<Allotment.Pane
|
<Allotment.Pane
|
||||||
minSize={55}
|
minSize={175}
|
||||||
preferredSize="35%"
|
preferredSize="35%"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -55,49 +53,40 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___9o87uj0_0000000 ffefeo0"
|
className="___77lcry0_0000000 f10pi13n"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
className="___1rwkz4r_0000000 f1euv43f f1l8gmrm f1e31b4d f150nix6 fy6ml6n f19g0ac"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
appearance="transparent"
|
||||||
|
aria-label="Refresh"
|
||||||
|
icon={<ArrowClockwise16Filled />}
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
size="small"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"height": "100%",
|
"color": undefined,
|
||||||
"width": "calc(100% + -11px)",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="___9o87uj0_0000000 ffefeo0"
|
||||||
>
|
>
|
||||||
<DocumentsTableComponent
|
<DocumentsTableComponent
|
||||||
collection={
|
columnHeaders={
|
||||||
{
|
{
|
||||||
"databaseId": "databaseId",
|
"idHeader": "id",
|
||||||
"id": [Function],
|
"partitionKeyHeaders": [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
columnDefinitions={
|
isSelectionDisabled={true}
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "id",
|
|
||||||
"isPartitionKey": false,
|
|
||||||
"label": "id",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
defaultColumnSelection={
|
|
||||||
[
|
|
||||||
"id",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
isColumnSelectionDisabled={false}
|
|
||||||
isRowSelectionDisabled={true}
|
|
||||||
items={[]}
|
items={[]}
|
||||||
onColumnSelectionChange={[Function]}
|
|
||||||
onItemClicked={[Function]}
|
onItemClicked={[Function]}
|
||||||
onRefreshTable={[Function]}
|
|
||||||
onSelectedRowsChange={[Function]}
|
onSelectedRowsChange={[Function]}
|
||||||
selectedColumnIds={
|
|
||||||
[
|
|
||||||
"id",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
selectedRows={
|
selectedRows={
|
||||||
Set {
|
Set {
|
||||||
0,
|
0,
|
||||||
@@ -106,10 +95,10 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
<Allotment.Pane
|
<Allotment.Pane
|
||||||
minSize={30}
|
minSize={300}
|
||||||
|
preferredSize="65%"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -152,6 +152,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
ariaLabel: saveLabel,
|
ariaLabel: saveLabel,
|
||||||
children: saveButtonChildren.length && [
|
children: saveButtonChildren.length && [
|
||||||
{
|
{
|
||||||
|
iconName: "Save",
|
||||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||||
commandButtonLabel: saveLabel,
|
commandButtonLabel: saveLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
createTableColumn,
|
createTableColumn,
|
||||||
tokens,
|
tokens,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { ErrorCircleFilled, MoreHorizontalRegular, QuestionRegular, WarningFilled } from "@fluentui/react-icons";
|
import { ErrorCircleFilled, MoreHorizontalRegular, WarningFilled } from "@fluentui/react-icons";
|
||||||
import QueryError, { QueryErrorSeverity, compareSeverity } from "Common/QueryError";
|
import QueryError, { QueryErrorSeverity, compareSeverity } from "Common/QueryError";
|
||||||
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||||
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
||||||
@@ -34,32 +34,25 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "code",
|
columnId: "code",
|
||||||
compare: (item1, item2) => item1.code.localeCompare(item2.code),
|
compare: (item1, item2) => item1.code.localeCompare(item2.code),
|
||||||
renderHeaderCell: () => "Code",
|
renderHeaderCell: () => null,
|
||||||
renderCell: (item) => <TableCellLayout truncate>{item.code}</TableCellLayout>,
|
renderCell: (item) => item.code,
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "severity",
|
columnId: "severity",
|
||||||
compare: (item1, item2) => compareSeverity(item1.severity, item2.severity),
|
compare: (item1, item2) => compareSeverity(item1.severity, item2.severity),
|
||||||
renderHeaderCell: () => "Severity",
|
renderHeaderCell: () => null,
|
||||||
renderCell: (item) => (
|
renderCell: (item) => <TableCellLayout media={severityIcons[item.severity]}>{item.severity}</TableCellLayout>,
|
||||||
<TableCellLayout truncate media={severityIcons[item.severity]}>
|
|
||||||
{item.severity}
|
|
||||||
</TableCellLayout>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "location",
|
columnId: "location",
|
||||||
compare: (item1, item2) => item1.location?.start?.offset - item2.location?.start?.offset,
|
compare: (item1, item2) => item1.location?.start?.offset - item2.location?.start?.offset,
|
||||||
renderHeaderCell: () => "Location",
|
renderHeaderCell: () => "Location",
|
||||||
renderCell: (item) => (
|
renderCell: (item) =>
|
||||||
<TableCellLayout truncate>
|
item.location
|
||||||
{item.location
|
|
||||||
? item.location.start.lineNumber
|
? item.location.start.lineNumber
|
||||||
? `Line ${item.location.start.lineNumber}`
|
? `Line ${item.location.start.lineNumber}`
|
||||||
: "<unknown>"
|
: "<unknown>"
|
||||||
: "<no location>"}
|
: "<no location>",
|
||||||
</TableCellLayout>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "message",
|
columnId: "message",
|
||||||
@@ -67,20 +60,8 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
renderHeaderCell: () => "Message",
|
renderHeaderCell: () => "Message",
|
||||||
renderCell: (item) => (
|
renderCell: (item) => (
|
||||||
<div className={styles.errorListMessageCell}>
|
<div className={styles.errorListMessageCell}>
|
||||||
<div className={styles.errorListMessage} title={item.message}>
|
<div className={styles.errorListMessage}>{item.message}</div>
|
||||||
{item.message}
|
<div>
|
||||||
</div>
|
|
||||||
<div className={styles.errorListMessageActions}>
|
|
||||||
{item.helpLink && (
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
aria-label="Help"
|
|
||||||
appearance="subtle"
|
|
||||||
icon={<QuestionRegular />}
|
|
||||||
href={item.helpLink}
|
|
||||||
target="_blank"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
aria-label="Details"
|
aria-label="Details"
|
||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
@@ -95,9 +76,9 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
|
|
||||||
const columnSizingOptions: TableColumnSizingOptions = {
|
const columnSizingOptions: TableColumnSizingOptions = {
|
||||||
code: {
|
code: {
|
||||||
minWidth: 90,
|
minWidth: 75,
|
||||||
idealWidth: 90,
|
idealWidth: 75,
|
||||||
defaultWidth: 90,
|
defaultWidth: 75,
|
||||||
},
|
},
|
||||||
severity: {
|
severity: {
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { fireEvent, render } from "@testing-library/react";
|
|||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
|
||||||
import {
|
import {
|
||||||
IQueryTabComponentProps,
|
IQueryTabComponentProps,
|
||||||
QueryTabComponent,
|
QueryTabComponent,
|
||||||
QueryTabCopilotComponent,
|
QueryTabCopilotComponent,
|
||||||
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -18,24 +16,6 @@ import React from "react";
|
|||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact");
|
jest.mock("Explorer/Controls/Editor/EditorReact");
|
||||||
|
|
||||||
const loadState = (path: StorePath) => {
|
|
||||||
if (
|
|
||||||
path.componentName === AppStateComponentNames.QueryCopilot &&
|
|
||||||
path.subComponentName === CopilotSubComponentNames.toggleStatus
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
|
||||||
loadState,
|
|
||||||
AppStateComponentNames: {
|
|
||||||
QueryCopilot: "QueryCopilot",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("QueryTabComponent", () => {
|
describe("QueryTabComponent", () => {
|
||||||
const mockStore = useQueryCopilot.getState();
|
const mockStore = useQueryCopilot.getState();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -52,7 +32,7 @@ describe("QueryTabComponent", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const propsMock: Readonly<IQueryTabComponentProps> = {
|
const propsMock: Readonly<IQueryTabComponentProps> = {
|
||||||
collection: { databaseId: "CopilotSampleDB" },
|
collection: { databaseId: "CopilotSampleDb" },
|
||||||
onTabAccessor: () => jest.fn(),
|
onTabAccessor: () => jest.fn(),
|
||||||
isExecutionError: false,
|
isExecutionError: false,
|
||||||
tabId: "mockTabId",
|
tabId: "mockTabId",
|
||||||
@@ -70,17 +50,6 @@ describe("QueryTabComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("copilot should be enabled by default when tab is active", () => {
|
it("copilot should be enabled by default when tab is active", () => {
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
name: "name",
|
|
||||||
properties: undefined,
|
|
||||||
id: "",
|
|
||||||
location: "",
|
|
||||||
type: "",
|
|
||||||
kind: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useQueryCopilot.getState().setCopilotEnabled(true);
|
useQueryCopilot.getState().setCopilotEnabled(true);
|
||||||
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
|
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
|
||||||
const activeTab = new TabsBase({
|
const activeTab = new TabsBase({
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import { SplitterDirection } from "Common/Splitter";
|
|||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
|
||||||
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
@@ -48,6 +46,7 @@ import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
|||||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import * as StringUtility from "../../../Shared/StringUtility";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
@@ -55,6 +54,7 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
|||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
@@ -209,7 +209,13 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
|
|
||||||
private _queryCopilotActive(): boolean {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
return readCopilotToggleStatus(userContext.databaseAccount);
|
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
||||||
|
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
||||||
|
);
|
||||||
|
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
||||||
|
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
||||||
|
: true;
|
||||||
|
return copilotInitialActive;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -578,7 +584,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
private _toggleCopilot = (active: boolean) => {
|
private _toggleCopilot = (active: boolean) => {
|
||||||
this.setState({ copilotActive: active });
|
this.setState({ copilotActive: active });
|
||||||
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
||||||
saveCopilotToggleStatus(userContext.databaseAccount, active);
|
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, active.toString());
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
|
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
|
||||||
databaseName: this.props.collection.databaseId,
|
databaseName: this.props.collection.databaseId,
|
||||||
|
|||||||
@@ -72,11 +72,6 @@ export const useQueryTabStyles = makeStyles({
|
|||||||
metricsGridButtons: {
|
metricsGridButtons: {
|
||||||
...cosmosShorthands.borderTop(),
|
...cosmosShorthands.borderTop(),
|
||||||
},
|
},
|
||||||
errorListTableCell: {
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
},
|
|
||||||
errorListMessageCell: {
|
errorListMessageCell: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -85,12 +80,5 @@ export const useQueryTabStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
errorListMessage: {
|
errorListMessage: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
},
|
|
||||||
errorListMessageActions: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||||
import { Pivot, PivotItem } from "@fluentui/react";
|
import { Pivot, PivotItem } from "@fluentui/react";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
@@ -16,6 +15,7 @@ import { useTabs } from "../../../hooks/useTabs";
|
|||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import ScriptTabBase from "../ScriptTabBase";
|
import ScriptTabBase from "../ScriptTabBase";
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
import { IMessageBarStyles, Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { configContext } from "ConfigContext";
|
import { Platform, configContext, updateConfigContext } from "ConfigContext";
|
||||||
import { IpRule } from "Contracts/DataModels";
|
import { IpRule } from "Contracts/DataModels";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { CommandBarV2 } from "Explorer/Menus/CommandBarV2/CommandBarV2";
|
|
||||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||||
@@ -17,6 +16,7 @@ import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
|||||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
|
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
@@ -37,6 +37,9 @@ interface TabsProps {
|
|||||||
|
|
||||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
||||||
|
const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState<boolean>(
|
||||||
|
userContext.apiType === "SQL" && configContext.platform !== Platform.Fabric && !hasRUThresholdBeenConfigured(),
|
||||||
|
);
|
||||||
const [
|
const [
|
||||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
@@ -84,6 +87,30 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
{networkSettingsWarning}
|
{networkSettingsWarning}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
{showRUThresholdMessageBar && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.info}
|
||||||
|
onDismiss={() => {
|
||||||
|
setShowRUThresholdMessageBar(false);
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
...defaultMessageBarStyles,
|
||||||
|
innerText: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove
|
||||||
|
the limit, go to the Settings cog on the right and find "RU Threshold".`}
|
||||||
|
<Link
|
||||||
|
className="underlinedLink"
|
||||||
|
href="https://review.learn.microsoft.com/en-us/azure/cosmos-db/data-explorer?branch=main#configure-request-unit-threshold"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
@@ -92,7 +119,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{`We have migrated our middleware to new infrastructure. To avoid issues with Data Explorer access, please
|
{`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please
|
||||||
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
@@ -107,7 +134,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="tabPanesContainer">
|
<div className="tabPanesContainer">
|
||||||
{userContext.features.commandBarV2 && <CommandBarV2 explorer={explorer} />}
|
|
||||||
{activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)}
|
{activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)}
|
||||||
{openedTabs.map((tab) => (
|
{openedTabs.map((tab) => (
|
||||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||||
@@ -372,6 +398,12 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|||||||
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (ipRulesIncludeMongoProxy) {
|
||||||
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return !ipRulesIncludeMongoProxy;
|
return !ipRulesIncludeMongoProxy;
|
||||||
} else if (userContext.apiType === "Cassandra") {
|
} else if (userContext.apiType === "Cassandra") {
|
||||||
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||||
@@ -390,6 +422,12 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|||||||
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (ipRulesIncludeCassandraProxy) {
|
||||||
|
updateConfigContext({
|
||||||
|
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return !ipRulesIncludeCassandraProxy;
|
return !ipRulesIncludeCassandraProxy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as ThemeUtility from "../../Common/ThemeUtility";
|
import * as ThemeUtility from "../../Common/ThemeUtility";
|
||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -10,6 +10,7 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
|||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
// TODO: Use specific actions for logging telemetry data
|
// TODO: Use specific actions for logging telemetry data
|
||||||
@@ -27,6 +28,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
public tabPath: ko.Observable<string>;
|
public tabPath: ko.Observable<string>;
|
||||||
public isExecutionError = ko.observable(false);
|
public isExecutionError = ko.observable(false);
|
||||||
public isExecuting = ko.observable(false);
|
public isExecuting = ko.observable(false);
|
||||||
|
public pendingNotification?: ko.Observable<DataModels.Notification>;
|
||||||
protected _theme: string;
|
protected _theme: string;
|
||||||
public onLoadStartKey: number;
|
public onLoadStartKey: number;
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
this.tabPath =
|
this.tabPath =
|
||||||
this.collection &&
|
this.collection &&
|
||||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`);
|
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`);
|
||||||
|
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
||||||
this.onLoadStartKey = options.onLoadStartKey;
|
this.onLoadStartKey = options.onLoadStartKey;
|
||||||
this.closeTabButton = {
|
this.closeTabButton = {
|
||||||
enabled: ko.computed<boolean>(() => {
|
enabled: ko.computed<boolean>(() => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { TriggerDefinition } from "@azure/cosmos";
|
import { TriggerDefinition } from "@azure/cosmos";
|
||||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
@@ -15,6 +14,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|||||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../Controls/Editor/EditorReact";
|
import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import TriggerTab from "./TriggerTab";
|
import TriggerTab from "./TriggerTab";
|
||||||
|
|
||||||
const triggerTypeOptions: IDropdownOption[] = [
|
const triggerTypeOptions: IDropdownOption[] = [
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { Label, TextField } from "@fluentui/react";
|
import { Label, TextField } from "@fluentui/react";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
@@ -14,6 +13,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../Controls/Editor/EditorReact";
|
import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
|
import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
|
||||||
|
|
||||||
interface IUserDefinedFunctionTabContentState {
|
interface IUserDefinedFunctionTabContentState {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import React from "react";
|
|||||||
import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme";
|
import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme";
|
||||||
|
|
||||||
export const LayoutConstants = {
|
export const LayoutConstants = {
|
||||||
rowHeight: 32,
|
rowHeight: 36,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Our CosmosFluentProvider has the same props as a FluentProvider.
|
// Our CosmosFluentProvider has the same props as a FluentProvider.
|
||||||
@@ -91,30 +91,15 @@ const appThemePortalBrandRamp: BrandVariants = {
|
|||||||
160: "#CDD8EF",
|
160: "#CDD8EF",
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum LayoutSize {
|
const cosmosThemeElements = {
|
||||||
Compact,
|
layoutRowHeight: `${LayoutConstants.rowHeight}px`,
|
||||||
// TODO: Cozy and Roomy layouts.
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CosmosThemeElements {
|
|
||||||
layoutRowHeight: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CosmosTheme = Theme & CosmosThemeElements;
|
|
||||||
|
|
||||||
const sizeMappings: Record<LayoutSize, Partial<Theme> & CosmosThemeElements> = {
|
|
||||||
[LayoutSize.Compact]: {
|
|
||||||
layoutRowHeight: "32px",
|
|
||||||
fontSizeBase300: "13px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const cosmosTheme = {
|
|
||||||
sidebarMinimumWidth: "200px",
|
sidebarMinimumWidth: "200px",
|
||||||
sidebarInitialWidth: "300px",
|
sidebarInitialWidth: "300px",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tokens = themeToTokensObject({ ...webLightTheme, ...cosmosTheme, ...sizeMappings[LayoutSize.Compact] });
|
export type CosmosTheme = Theme & typeof cosmosThemeElements;
|
||||||
|
|
||||||
|
export const tokens = themeToTokensObject({ ...webLightTheme, ...cosmosThemeElements });
|
||||||
|
|
||||||
export const cosmosShorthands = {
|
export const cosmosShorthands = {
|
||||||
border: () => shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
border: () => shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
@@ -132,7 +117,6 @@ export function getPlatformTheme(platform: Platform): CosmosTheme {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...baseTheme,
|
...baseTheme,
|
||||||
...cosmosTheme,
|
...cosmosThemeElements,
|
||||||
...sizeMappings[LayoutSize.Compact], // TODO: Allow for different layout sizes.
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
|
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
||||||
@@ -24,6 +25,7 @@ import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
|||||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
||||||
import ConflictsTab from "../Tabs/ConflictsTab";
|
import ConflictsTab from "../Tabs/ConflictsTab";
|
||||||
import GraphTab from "../Tabs/GraphTab";
|
import GraphTab from "../Tabs/GraphTab";
|
||||||
@@ -1018,6 +1020,41 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
||||||
|
if (!this.container) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
|
||||||
|
if (!notifications || notifications.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.find(notifications, (notification: DataModels.Notification) => {
|
||||||
|
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
||||||
|
return (
|
||||||
|
notification.kind === "message" &&
|
||||||
|
notification.collectionName === this.id() &&
|
||||||
|
notification.description &&
|
||||||
|
throughputUpdateRegExp.test(notification.description)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(
|
||||||
|
JSON.stringify({
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
accountName: userContext?.databaseAccount,
|
||||||
|
databaseName: this.databaseId,
|
||||||
|
collectionName: this.id(),
|
||||||
|
}),
|
||||||
|
"Settings tree node",
|
||||||
|
);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
|
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
|
||||||
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
|
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import * as _ from "underscore";
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
import { readCollections, readCollectionsWithPagination } from "../../Common/dataAccess/readCollections";
|
import { readCollections, readCollectionsWithPagination } from "../../Common/dataAccess/readCollections";
|
||||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
@@ -74,6 +76,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
await useDatabases.getState().loadAllOffers();
|
await useDatabases.getState().loadAllOffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
|
||||||
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2;
|
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2;
|
||||||
const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id());
|
const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id());
|
||||||
let settingsTab = matchingTabs?.[0] as DatabaseSettingsTabV2;
|
let settingsTab = matchingTabs?.[0] as DatabaseSettingsTabV2;
|
||||||
@@ -84,8 +87,10 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.Tab,
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
tabTitle: "Scale",
|
tabTitle: "Scale",
|
||||||
});
|
});
|
||||||
|
pendingNotificationsPromise.then(
|
||||||
try {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(data: any) => {
|
||||||
|
const pendingNotification: DataModels.Notification = data?.[0];
|
||||||
const tabOptions: ViewModels.TabOptions = {
|
const tabOptions: ViewModels.TabOptions = {
|
||||||
tabKind,
|
tabKind,
|
||||||
title: "Scale",
|
title: "Scale",
|
||||||
@@ -96,8 +101,10 @@ export default class Database implements ViewModels.Database {
|
|||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
};
|
};
|
||||||
settingsTab = new DatabaseSettingsTabV2(tabOptions);
|
settingsTab = new DatabaseSettingsTabV2(tabOptions);
|
||||||
|
settingsTab.pendingNotification(pendingNotification);
|
||||||
useTabs.getState().activateNewTab(settingsTab);
|
useTabs.getState().activateNewTab(settingsTab);
|
||||||
} catch (error) {
|
},
|
||||||
|
(error) => {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
Action.Tab,
|
Action.Tab,
|
||||||
@@ -114,9 +121,19 @@ export default class Database implements ViewModels.Database {
|
|||||||
);
|
);
|
||||||
logConsoleError(`Error while fetching database settings for database ${this.id()}: ${errorMessage}`);
|
logConsoleError(`Error while fetching database settings for database ${this.id()}: ${errorMessage}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
pendingNotificationsPromise.then(
|
||||||
|
(pendingNotification: DataModels.Notification) => {
|
||||||
|
settingsTab.pendingNotification(pendingNotification);
|
||||||
useTabs.getState().activateTab(settingsTab);
|
useTabs.getState().activateTab(settingsTab);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
settingsTab.pendingNotification(undefined);
|
||||||
|
useTabs.getState().activateTab(settingsTab);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,6 +260,42 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
||||||
|
if (!this.container) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
|
||||||
|
if (!notifications || notifications.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.find(notifications, (notification: DataModels.Notification) => {
|
||||||
|
const throughputUpdateRegExp = new RegExp("Throughput update (.*) in progress");
|
||||||
|
return (
|
||||||
|
notification.kind === "message" &&
|
||||||
|
!notification.collectionName &&
|
||||||
|
notification.databaseName === this.id() &&
|
||||||
|
notification.description &&
|
||||||
|
throughputUpdateRegExp.test(notification.description)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(
|
||||||
|
JSON.stringify({
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
accountName: userContext?.databaseAccount,
|
||||||
|
databaseName: this.id(),
|
||||||
|
collectionName: this.id(),
|
||||||
|
}),
|
||||||
|
"Settings tree node",
|
||||||
|
);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getDeltaCollections(updatedCollectionsList: DataModels.Collection[]): {
|
private getDeltaCollections(updatedCollectionsList: DataModels.Collection[]): {
|
||||||
toAdd: DataModels.Collection[];
|
toAdd: DataModels.Collection[];
|
||||||
toDelete: Collection[];
|
toDelete: Collection[];
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
|
||||||
import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil";
|
import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil";
|
||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
@@ -29,6 +27,8 @@ import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFacto
|
|||||||
import { useDialog } from "../Controls/Dialog";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent";
|
import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
@@ -229,7 +229,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -72,9 +69,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -98,9 +92,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -111,9 +102,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"iconSrc": <SettingsRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"id": "",
|
"id": "",
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
@@ -145,9 +133,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -171,9 +156,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -264,9 +246,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -295,9 +274,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -369,9 +345,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -442,9 +415,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -468,9 +438,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -481,9 +448,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"iconSrc": <SettingsRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"id": "",
|
"id": "",
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
@@ -546,9 +510,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -572,9 +533,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -696,9 +654,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -727,9 +682,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -754,9 +706,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -775,9 +724,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -801,9 +747,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -823,9 +766,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -849,9 +789,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -871,9 +808,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -902,9 +836,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -1045,9 +976,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -1148,9 +1076,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -1174,9 +1099,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -1187,9 +1109,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"iconSrc": <SettingsRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"id": "",
|
"id": "",
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
@@ -1282,9 +1201,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -1308,9 +1224,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -1462,9 +1375,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -1493,9 +1403,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -1636,9 +1543,6 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -1734,9 +1638,6 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -1760,9 +1661,6 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -1773,9 +1671,6 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"iconSrc": <SettingsRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"id": "",
|
"id": "",
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
@@ -1868,9 +1763,6 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -1894,9 +1786,6 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -2048,9 +1937,6 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -2079,9 +1965,6 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DatabaseRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -2103,9 +1986,6 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "testCollection",
|
"label": "testCollection",
|
||||||
@@ -2141,9 +2021,6 @@ exports[`createSampleDataTreeNodes creates the expected tree nodes 1`] = `
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <DocumentMultipleRegular
|
|
||||||
fontSize={16}
|
|
||||||
/>,
|
|
||||||
"isExpanded": false,
|
"isExpanded": false,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "testCollection",
|
"label": "testCollection",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CapabilityNames } from "Common/Constants";
|
|||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
|
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons";
|
|
||||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
||||||
import Trigger from "Explorer/Tree/Trigger";
|
import Trigger from "Explorer/Tree/Trigger";
|
||||||
@@ -10,7 +7,6 @@ import { useDatabases } from "Explorer/useDatabases";
|
|||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import React from "react";
|
|
||||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
import { Platform, configContext } from "../../ConfigContext";
|
import { Platform, configContext } from "../../ConfigContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
@@ -18,6 +14,8 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|
||||||
@@ -27,10 +25,6 @@ export const shouldShowScriptNodes = (): boolean => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
|
||||||
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
|
||||||
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
|
||||||
|
|
||||||
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
||||||
const updatedSampleTree: TreeNode = {
|
const updatedSampleTree: TreeNode = {
|
||||||
label: sampleDataResourceTokenCollection.databaseId,
|
label: sampleDataResourceTokenCollection.databaseId,
|
||||||
@@ -42,7 +36,6 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie
|
|||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
||||||
iconSrc: TreeCollectionIcon,
|
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
||||||
useCommandBar.getState().setContextButtons([]);
|
useCommandBar.getState().setContextButtons([]);
|
||||||
@@ -98,7 +91,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.onDocumentDBDocumentsClick();
|
collection.onDocumentDBDocumentsClick();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
@@ -111,7 +104,6 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
children,
|
children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
iconSrc: TreeCollectionIcon,
|
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
@@ -141,7 +133,6 @@ export const createDatabaseTreeNodes = (
|
|||||||
databaseNode.children.push({
|
databaseNode.children.push({
|
||||||
id: database.isSampleDB ? "sampleScaleSettings" : "",
|
id: database.isSampleDB ? "sampleScaleSettings" : "",
|
||||||
label: "Scale",
|
label: "Scale",
|
||||||
iconSrc: TreeSettingsIcon,
|
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
.getState()
|
.getState()
|
||||||
@@ -178,7 +169,6 @@ export const createDatabaseTreeNodes = (
|
|||||||
children: [],
|
children: [],
|
||||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||||
iconSrc: TreeDatabaseIcon,
|
|
||||||
onExpanded: async () => {
|
onExpanded: async () => {
|
||||||
useSelectedNode.getState().setSelectedNode(database);
|
useSelectedNode.getState().setSelectedNode(database);
|
||||||
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
||||||
@@ -229,12 +219,11 @@ export const buildCollectionNode = (
|
|||||||
children: children,
|
children: children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||||
iconSrc: TreeCollectionIcon,
|
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
},
|
},
|
||||||
onExpanded: async () => {
|
onExpanded: async () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
@@ -282,7 +271,7 @@ const buildCollectionNodeChildren = (
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
@@ -20,11 +20,9 @@ import "../externals/jquery.typeahead.min.css";
|
|||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { Platform } from "ConfigContext";
|
import { Platform } from "ConfigContext";
|
||||||
import { CommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { SidebarContainer } from "Explorer/Sidebar";
|
import { SidebarContainer } from "Explorer/Sidebar";
|
||||||
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import "allotment/dist/style.css";
|
import "allotment/dist/style.css";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
@@ -50,6 +48,7 @@ import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
|||||||
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
||||||
|
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
|
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
|
||||||
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
||||||
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
||||||
@@ -87,7 +86,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
{/* Main Command Bar - Start */}
|
{/* Main Command Bar - Start */}
|
||||||
{!userContext.features.commandBarV2 && <CommandBar container={explorer} />}
|
<CommandBar container={explorer} />
|
||||||
{/* Collections Tree and Tabs - Begin */}
|
{/* Collections Tree and Tabs - Begin */}
|
||||||
<SidebarContainer explorer={explorer} />
|
<SidebarContainer explorer={explorer} />
|
||||||
{/* Collections Tree and Tabs - End */}
|
{/* Collections Tree and Tabs - End */}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const isAccountRestrictedForConnectionStringLogin = async (connectionStri
|
|||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append(HttpHeaders.connectionString, connectionString);
|
headers.append(HttpHeaders.connectionString, connectionString);
|
||||||
|
|
||||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.AccountRestrictions)
|
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||||
: configContext.BACKEND_ENDPOINT;
|
: configContext.BACKEND_ENDPOINT;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import FeedbackIcon from "../../../../images/Feedback.svg";
|
import FeedbackIcon from "../../../../images/Feedback.svg";
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FeedbackCommandButton: React.FunctionComponent = () => {
|
export const FeedbackCommandButton: React.FunctionComponent = () => {
|
||||||
return (
|
return (
|
||||||
<div className="feedbackConnectSettingIcons">
|
<div className="feedbackConnectSettingIcons">
|
||||||
<div className="commandButtonReact">
|
<CommandButtonComponent
|
||||||
<a href="#" title="Send feedback" aria-haspopup="dialog" onClick={onClick}>
|
id="commandbutton-feedback"
|
||||||
<img src={FeedbackIcon} alt="Send feedback" />
|
iconSrc={FeedbackIcon}
|
||||||
</a>
|
iconAlt="feeback button"
|
||||||
</div>
|
onCommandClick={() =>
|
||||||
|
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback")
|
||||||
|
}
|
||||||
|
ariaLabel="feeback button"
|
||||||
|
tooltipText="Send feedback"
|
||||||
|
hasPopup={true}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe("parseResourceTokenConnectionString", () => {
|
|||||||
collectionId: "fakeCollectionId",
|
collectionId: "fakeCollectionId",
|
||||||
databaseId: "fakeDatabaseId",
|
databaseId: "fakeDatabaseId",
|
||||||
partitionKey: undefined,
|
partitionKey: undefined,
|
||||||
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken",
|
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ describe("parseResourceTokenConnectionString", () => {
|
|||||||
collectionId: "fakeCollectionId",
|
collectionId: "fakeCollectionId",
|
||||||
databaseId: "fakeDatabaseId",
|
databaseId: "fakeDatabaseId",
|
||||||
partitionKey: "fakePartitionKey",
|
partitionKey: "fakePartitionKey",
|
||||||
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken",
|
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ export function parseResourceTokenConnectionString(connectionString: string): Pa
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resourceToken && resourceToken.endsWith(";")) {
|
|
||||||
resourceToken = resourceToken.substring(0, resourceToken.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accountEndpoint,
|
accountEndpoint,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
readonly commandBarV2: boolean;
|
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -109,7 +108,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
commandBarV2: "true" === get("commandbarv2"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
import {
|
|
||||||
AppStateComponentNames,
|
|
||||||
createKeyFromPath,
|
|
||||||
deleteState,
|
|
||||||
loadState,
|
|
||||||
MAX_ENTRY_NB,
|
|
||||||
PATH_SEPARATOR,
|
|
||||||
saveState,
|
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|
||||||
|
|
||||||
jest.mock("Shared/StorageUtility", () => ({
|
|
||||||
LocalStorageUtility: {
|
|
||||||
getEntryObject: jest.fn(),
|
|
||||||
setEntryObject: jest.fn(),
|
|
||||||
},
|
|
||||||
StorageKey: {
|
|
||||||
AppState: "AppState",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("AppStatePersistenceUtility", () => {
|
|
||||||
const storePath = {
|
|
||||||
componentName: AppStateComponentNames.DocumentsTab,
|
|
||||||
subComponentName: "b",
|
|
||||||
globalAccountName: "c",
|
|
||||||
databaseName: "d",
|
|
||||||
containerName: "e",
|
|
||||||
};
|
|
||||||
const key = createKeyFromPath(storePath);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({
|
|
||||||
key0: {
|
|
||||||
schemaVersion: 1,
|
|
||||||
timestamp: 0,
|
|
||||||
data: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("saveState()", () => {
|
|
||||||
const testState = { aa: 1, bb: "2", cc: [3, 4] };
|
|
||||||
|
|
||||||
it("should save state", () => {
|
|
||||||
saveState(storePath, testState);
|
|
||||||
expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledTimes(1);
|
|
||||||
expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledWith(StorageKey.AppState, expect.any(Object));
|
|
||||||
|
|
||||||
const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
|
||||||
expect(passedState[key].data).toHaveProperty("aa", 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should save state with timestamp", () => {
|
|
||||||
saveState(storePath, testState);
|
|
||||||
const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
|
||||||
expect(passedState[key]).toHaveProperty("timestamp");
|
|
||||||
expect(passedState[key].timestamp).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add state to existing state", () => {
|
|
||||||
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({
|
|
||||||
key0: {
|
|
||||||
schemaVersion: 1,
|
|
||||||
timestamp: 0,
|
|
||||||
data: { dd: 5 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
saveState(storePath, testState);
|
|
||||||
const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
|
||||||
expect(passedState["key0"].data).toHaveProperty("dd", 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove the oldest entry when the number of entries exceeds the limit", () => {
|
|
||||||
// Fill up storage with MAX entries
|
|
||||||
const currentAppState = {};
|
|
||||||
for (let i = 0; i < MAX_ENTRY_NB; i++) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(currentAppState as any)[`key${i}`] = {
|
|
||||||
schemaVersion: 1,
|
|
||||||
timestamp: i,
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(currentAppState);
|
|
||||||
|
|
||||||
saveState(storePath, testState);
|
|
||||||
|
|
||||||
// Verify that the new entry is saved
|
|
||||||
const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
|
||||||
expect(passedState[key].data).toHaveProperty("aa", 1);
|
|
||||||
|
|
||||||
// Verify that the oldest entry is removed (smallest timestamp)
|
|
||||||
const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
|
||||||
expect(Object.keys(passedAppState).length).toBe(MAX_ENTRY_NB);
|
|
||||||
expect(passedAppState).not.toHaveProperty("key0");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not remove the oldest entry when the number of entries does not exceed the limit", () => {
|
|
||||||
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({
|
|
||||||
key0: {
|
|
||||||
schemaVersion: 1,
|
|
||||||
timestamp: 0,
|
|
||||||
data: {},
|
|
||||||
},
|
|
||||||
key1: {
|
|
||||||
schemaVersion: 1,
|
|
||||||
timestamp: 1,
|
|
||||||
data: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
saveState(storePath, testState);
|
|
||||||
const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
|
||||||
expect(Object.keys(passedAppState).length).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("loadState()", () => {
|
|
||||||
it("should load state", () => {
|
|
||||||
const data = { aa: 1, bb: "2", cc: [3, 4] };
|
|
||||||
const testState = {
|
|
||||||
[key]: {
|
|
||||||
schemaVersion: 1,
|
|
||||||
timestamp: 0,
|
|
||||||
data,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(testState);
|
|
||||||
const state = loadState(storePath);
|
|
||||||
expect(state).toEqual(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return undefined if the state is not found", () => {
|
|
||||||
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(null);
|
|
||||||
const state = loadState(storePath);
|
|
||||||
expect(state).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("deleteState()", () => {
|
|
||||||
it("should delete state", () => {
|
|
||||||
const key = createKeyFromPath(storePath);
|
|
||||||
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({
|
|
||||||
[key]: {
|
|
||||||
schemaVersion: 1,
|
|
||||||
timestamp: 0,
|
|
||||||
data: {},
|
|
||||||
},
|
|
||||||
otherKey: {
|
|
||||||
schemaVersion: 2,
|
|
||||||
timestamp: 0,
|
|
||||||
data: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteState(storePath);
|
|
||||||
expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledTimes(1);
|
|
||||||
const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
|
||||||
expect(passedAppState).not.toHaveProperty(key);
|
|
||||||
expect(passedAppState).toHaveProperty("otherKey");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("createKeyFromPath()", () => {
|
|
||||||
it("should create path that contains all components", () => {
|
|
||||||
const key = createKeyFromPath(storePath);
|
|
||||||
expect(key).toContain(storePath.componentName);
|
|
||||||
expect(key).toContain(storePath.subComponentName);
|
|
||||||
expect(key).toContain(storePath.globalAccountName);
|
|
||||||
expect(key).toContain(storePath.databaseName);
|
|
||||||
expect(key).toContain(storePath.containerName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle components that include special characters", () => {
|
|
||||||
const storePath = {
|
|
||||||
componentName: AppStateComponentNames.DocumentsTab,
|
|
||||||
subComponentName: 'd"e"f',
|
|
||||||
globalAccountName: "g:hi{j",
|
|
||||||
databaseName: "a/b/c",
|
|
||||||
containerName: "https://blahblah.document.azure.com:443/",
|
|
||||||
};
|
|
||||||
const key = createKeyFromPath(storePath);
|
|
||||||
const segments = key.split(PATH_SEPARATOR);
|
|
||||||
expect(segments.length).toEqual(6); // There should be 5 segments
|
|
||||||
expect(segments[0]).toBe("");
|
|
||||||
|
|
||||||
const expectSubstringsInValue = (value: string, subStrings: string[]): boolean =>
|
|
||||||
subStrings.every((subString) => value.includes(subString));
|
|
||||||
|
|
||||||
expect(expectSubstringsInValue(segments[2], ["d", "e", "f"])).toBe(true);
|
|
||||||
expect(expectSubstringsInValue(segments[3], ["g", "hi", "j"])).toBe(true);
|
|
||||||
expect(expectSubstringsInValue(segments[4], ["a", "b", "c"])).toBe(true);
|
|
||||||
expect(expectSubstringsInValue(segments[5], ["https", "blahblah", "document", "com", "443"])).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|
||||||
|
|
||||||
// The component name whose state is being saved. Component name must not include special characters.
|
|
||||||
export enum AppStateComponentNames {
|
|
||||||
DocumentsTab = "DocumentsTab",
|
|
||||||
MostRecentActivity = "MostRecentActivity",
|
|
||||||
QueryCopilot = "QueryCopilot",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
|
||||||
const SCHEMA_VERSION = 1;
|
|
||||||
|
|
||||||
// Export for testing purposes
|
|
||||||
export const MAX_ENTRY_NB = 100_000; // Limit number of entries to 100k
|
|
||||||
|
|
||||||
export interface StateData {
|
|
||||||
schemaVersion: number;
|
|
||||||
timestamp: number;
|
|
||||||
data: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for testing purposes
|
|
||||||
export type StorePath = {
|
|
||||||
componentName: AppStateComponentNames;
|
|
||||||
subComponentName?: string;
|
|
||||||
globalAccountName?: string;
|
|
||||||
databaseName?: string;
|
|
||||||
containerName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load and save state data
|
|
||||||
export const loadState = (path: StorePath): unknown => {
|
|
||||||
const appState =
|
|
||||||
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
|
|
||||||
const key = createKeyFromPath(path);
|
|
||||||
return appState[key]?.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveState = (path: StorePath, state: unknown): void => {
|
|
||||||
// Retrieve state object
|
|
||||||
const appState =
|
|
||||||
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
|
|
||||||
const key = createKeyFromPath(path);
|
|
||||||
appState[key] = {
|
|
||||||
schemaVersion: SCHEMA_VERSION,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
data: state,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.keys(appState).length > MAX_ENTRY_NB) {
|
|
||||||
// Remove the oldest entry
|
|
||||||
const oldestKey = Object.keys(appState).reduce((oldest, current) =>
|
|
||||||
appState[current].timestamp < appState[oldest].timestamp ? current : oldest,
|
|
||||||
);
|
|
||||||
delete appState[oldestKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteState = (path: StorePath): void => {
|
|
||||||
// Retrieve state object
|
|
||||||
const appState =
|
|
||||||
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
|
|
||||||
const key = createKeyFromPath(path);
|
|
||||||
delete appState[key];
|
|
||||||
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasState = (path: StorePath): boolean => {
|
|
||||||
return loadState(path) !== undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is for high-frequency state changes
|
|
||||||
let timeoutId: NodeJS.Timeout | undefined;
|
|
||||||
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ApplicationState {
|
|
||||||
[statePath: string]: StateData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderedPathSegments: (keyof StorePath)[] = [
|
|
||||||
"subComponentName",
|
|
||||||
"globalAccountName",
|
|
||||||
"databaseName",
|
|
||||||
"containerName",
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* /componentName/subComponentName/globalAccountName/databaseName/containerName/
|
|
||||||
* Any of the path segments can be "" except componentName
|
|
||||||
* Export for testing purposes
|
|
||||||
* @param path
|
|
||||||
*/
|
|
||||||
export const createKeyFromPath = (path: StorePath): string => {
|
|
||||||
let key = `${PATH_SEPARATOR}${encodeURIComponent(path.componentName)}`; // ComponentName is always there
|
|
||||||
orderedPathSegments.forEach((segment) => {
|
|
||||||
const segmentValue = path[segment as keyof StorePath];
|
|
||||||
key += `${PATH_SEPARATOR}${segmentValue !== undefined ? encodeURIComponent(segmentValue) : ""}`;
|
|
||||||
});
|
|
||||||
return key;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the entire app state key from local storage
|
|
||||||
*/
|
|
||||||
export const deleteAllStates = (): void => {
|
|
||||||
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
|
||||||
};
|
|
||||||
@@ -20,14 +20,3 @@ export const setEntryNumber = (key: StorageKey, value: number): void =>
|
|||||||
|
|
||||||
export const setEntryBoolean = (key: StorageKey, value: boolean): void =>
|
export const setEntryBoolean = (key: StorageKey, value: boolean): void =>
|
||||||
localStorage.setItem(StorageKey[key], value.toString());
|
localStorage.setItem(StorageKey[key], value.toString());
|
||||||
|
|
||||||
export const setEntryObject = (key: StorageKey, value: unknown): void => {
|
|
||||||
localStorage.setItem(StorageKey[key], JSON.stringify(value));
|
|
||||||
};
|
|
||||||
export const getEntryObject = <T>(key: StorageKey): T | null => {
|
|
||||||
const item = localStorage.getItem(StorageKey[key]);
|
|
||||||
if (item) {
|
|
||||||
return JSON.parse(item) as T;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -24,14 +24,12 @@ export enum StorageKey {
|
|||||||
MaxDegreeOfParellism,
|
MaxDegreeOfParellism,
|
||||||
IsGraphAutoVizDisabled,
|
IsGraphAutoVizDisabled,
|
||||||
TenantId,
|
TenantId,
|
||||||
MostRecentActivity, // deprecated
|
MostRecentActivity,
|
||||||
SetPartitionKeyUndefined,
|
SetPartitionKeyUndefined,
|
||||||
GalleryCalloutDismissed,
|
GalleryCalloutDismissed,
|
||||||
VisitedAccounts,
|
VisitedAccounts,
|
||||||
PriorityLevel,
|
PriorityLevel,
|
||||||
DocumentsTabPrefs,
|
|
||||||
DefaultQueryResultsView,
|
DefaultQueryResultsView,
|
||||||
AppState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasRUThresholdBeenConfigured = (): boolean => {
|
export const hasRUThresholdBeenConfigured = (): boolean => {
|
||||||
@@ -58,10 +56,10 @@ export const getRUThreshold = (): number => {
|
|||||||
|
|
||||||
export const getDefaultQueryResultsView = (): SplitterDirection => {
|
export const getDefaultQueryResultsView = (): SplitterDirection => {
|
||||||
const defaultQueryResultsViewRaw = LocalStorageUtility.getEntryString(StorageKey.DefaultQueryResultsView);
|
const defaultQueryResultsViewRaw = LocalStorageUtility.getEntryString(StorageKey.DefaultQueryResultsView);
|
||||||
if (defaultQueryResultsViewRaw === SplitterDirection.Vertical) {
|
if (defaultQueryResultsViewRaw === SplitterDirection.Horizontal) {
|
||||||
return SplitterDirection.Vertical;
|
|
||||||
}
|
|
||||||
return SplitterDirection.Horizontal;
|
return SplitterDirection.Horizontal;
|
||||||
|
}
|
||||||
|
return SplitterDirection.Vertical;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DefaultRUThreshold = 5000;
|
export const DefaultRUThreshold = 5000;
|
||||||
|
|||||||
@@ -139,9 +139,6 @@ export enum Action {
|
|||||||
QueryEdited,
|
QueryEdited,
|
||||||
ExecuteQueryGeneratedFromQueryCopilot,
|
ExecuteQueryGeneratedFromQueryCopilot,
|
||||||
DeleteDocuments,
|
DeleteDocuments,
|
||||||
ReadPersistedTabState,
|
|
||||||
SavePersistedTabState,
|
|
||||||
DeletePersistedTabState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionModifiers = {
|
export const ActionModifiers = {
|
||||||
|
|||||||
@@ -52,11 +52,7 @@ export const defaultAllowedArmEndpoints: ReadonlyArray<string> = [
|
|||||||
"https://management.chinacloudapi.cn",
|
"https://management.chinacloudapi.cn",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const allowedAadEndpoints: ReadonlyArray<string> = [
|
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
|
||||||
"https://login.microsoftonline.com/",
|
|
||||||
"https://login.microsoftonline.us/",
|
|
||||||
"https://login.partner.microsoftonline.cn/",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
||||||
"https://main.documentdb.ext.azure.com",
|
"https://main.documentdb.ext.azure.com",
|
||||||
@@ -78,13 +74,6 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
|
|||||||
//usnat: ["7.28.202.68"],
|
//usnat: ["7.28.202.68"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PortalBackendOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[PortalBackendEndpoints.Mpac]: ["13.91.105.215", "4.210.172.107"],
|
|
||||||
[PortalBackendEndpoints.Prod]: ["13.88.56.148", "40.91.218.243"],
|
|
||||||
[PortalBackendEndpoints.Fairfax]: ["52.247.163.6", "52.244.134.181"],
|
|
||||||
[PortalBackendEndpoints.Mooncake]: ["163.228.137.6", "143.64.170.142"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
||||||
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
||||||
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
||||||
@@ -175,28 +164,7 @@ export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
|||||||
PortalBackendEndpoints.Mpac,
|
PortalBackendEndpoints.Mpac,
|
||||||
PortalBackendEndpoints.Prod,
|
PortalBackendEndpoints.Prod,
|
||||||
],
|
],
|
||||||
[BackendApi.AccountRestrictions]: [
|
[BackendApi.AccountRestrictions]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
|
||||||
PortalBackendEndpoints.Development,
|
|
||||||
PortalBackendEndpoints.Mpac,
|
|
||||||
PortalBackendEndpoints.Prod,
|
|
||||||
],
|
|
||||||
[BackendApi.RuntimeProxy]: [
|
|
||||||
PortalBackendEndpoints.Development,
|
|
||||||
PortalBackendEndpoints.Mpac,
|
|
||||||
PortalBackendEndpoints.Prod,
|
|
||||||
],
|
|
||||||
[BackendApi.DisallowedLocations]: [
|
|
||||||
PortalBackendEndpoints.Development,
|
|
||||||
PortalBackendEndpoints.Mpac,
|
|
||||||
PortalBackendEndpoints.Prod,
|
|
||||||
PortalBackendEndpoints.Fairfax,
|
|
||||||
PortalBackendEndpoints.Mooncake,
|
|
||||||
],
|
|
||||||
[BackendApi.SampleData]: [
|
|
||||||
PortalBackendEndpoints.Development,
|
|
||||||
PortalBackendEndpoints.Mpac,
|
|
||||||
PortalBackendEndpoints.Prod,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user